summaryrefslogtreecommitdiff
path: root/qa
diff options
context:
space:
mode:
Diffstat (limited to 'qa')
-rw-r--r--qa/Gemfile12
-rw-r--r--qa/Gemfile.lock41
-rw-r--r--qa/chemlab-library-gitlab.gemspec22
-rw-r--r--qa/lib/gitlab.rb20
-rw-r--r--qa/lib/gitlab/page/group/settings/billing.rb19
-rw-r--r--qa/lib/gitlab/page/group/settings/billing.stub.rb107
-rw-r--r--qa/lib/gitlab/page/main/login.rb22
-rw-r--r--qa/lib/gitlab/page/main/login.stub.rb101
-rw-r--r--qa/lib/gitlab/page/subscriptions/new.rb41
-rw-r--r--qa/lib/gitlab/page/subscriptions/new.stub.rb545
-rw-r--r--qa/qa.rb13
-rw-r--r--qa/qa/fixtures/export.tar.gzbin306887 -> 308811 bytes
-rw-r--r--qa/qa/page/base.rb16
-rw-r--r--qa/qa/page/component/content_editor.rb54
-rw-r--r--qa/qa/page/component/import/gitlab.rb4
-rw-r--r--qa/qa/page/component/invite_members_modal.rb12
-rw-r--r--qa/qa/page/component/wiki.rb12
-rw-r--r--qa/qa/page/component/wiki_page_form.rb7
-rw-r--r--qa/qa/page/dashboard/projects.rb8
-rw-r--r--qa/qa/page/group/bulk_import.rb8
-rw-r--r--qa/qa/page/group/dependency_proxy.rb26
-rw-r--r--qa/qa/page/group/members.rb2
-rw-r--r--qa/qa/page/group/menu.rb110
-rw-r--r--qa/qa/page/group/settings/billing.rb13
-rw-r--r--qa/qa/page/merge_request/show.rb9
-rw-r--r--qa/qa/page/project/deployments/environments/show.rb23
-rw-r--r--qa/qa/page/project/import/github.rb96
-rw-r--r--qa/qa/page/project/new.rb4
-rw-r--r--qa/qa/page/project/registry/show.rb2
-rw-r--r--qa/qa/page/project/secure/configuration_form.rb31
-rw-r--r--qa/qa/page/project/wiki/edit.rb1
-rw-r--r--qa/qa/resource/base.rb21
-rw-r--r--qa/qa/resource/bulk_import_group.rb85
-rw-r--r--qa/qa/resource/group_base.rb2
-rw-r--r--qa/qa/resource/issue.rb41
-rw-r--r--qa/qa/resource/merge_request.rb13
-rw-r--r--qa/qa/resource/project.rb68
-rw-r--r--qa/qa/resource/project_imported_from_github.rb7
-rw-r--r--qa/qa/runtime/allure_report.rb21
-rw-r--r--qa/qa/runtime/api/request.rb6
-rw-r--r--qa/qa/runtime/application_settings.rb2
-rw-r--r--qa/qa/runtime/browser.rb3
-rw-r--r--qa/qa/runtime/env.rb4
-rw-r--r--qa/qa/runtime/feature.rb10
-rw-r--r--qa/qa/specs/features/api/1_manage/bulk_import_group_spec.rb102
-rw-r--r--qa/qa/specs/features/api/1_manage/import_github_repo_spec.rb2
-rw-r--r--qa/qa/specs/features/api/1_manage/import_large_github_repo_spec.rb362
-rw-r--r--qa/qa/specs/features/api/3_create/merge_request/push_options_target_branch_spec.rb12
-rw-r--r--qa/qa/specs/features/api/3_create/repository/project_archive_compare_spec.rb2
-rw-r--r--qa/qa/specs/features/api/5_package/container_registry_spec.rb16
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/group/bulk_import_group_spec.rb51
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb54
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb42
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/design_management/modify_design_content_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_diff_patch_spec.rb7
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/wiki/content_editor_spec.rb43
-rw-r--r--qa/qa/specs/features/browser_ui/4_verify/pipeline/include_local_config_file_paths_with_wildcard_spec.rb6
-rw-r--r--qa/qa/specs/features/browser_ui/4_verify/pipeline/merge_mr_when_pipline_is_blocked_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/4_verify/pipeline/mr_event_rule_pipeline_spec.rb81
-rw-r--r--qa/qa/specs/features/browser_ui/4_verify/pipeline/pass_dotenv_variables_to_downstream_via_bridge_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/4_verify/testing/view_code_coverage_spec.rb4
-rw-r--r--qa/qa/specs/features/browser_ui/5_package/container_registry_omnibus_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/5_package/dependency_proxy_spec.rb104
-rw-r--r--qa/qa/specs/features/browser_ui/5_package/online_garbage_collection_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/5_package/pypi_repository_spec.rb53
-rw-r--r--qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb68
-rw-r--r--qa/qa/specs/features/browser_ui/7_configure/kubernetes/kubernetes_integration_spec.rb6
-rw-r--r--qa/qa/specs/helpers/context_formatter.rb68
-rw-r--r--qa/qa/specs/helpers/context_selector.rb12
-rw-r--r--qa/qa/specs/helpers/quarantine.rb44
-rw-r--r--qa/qa/specs/helpers/quarantine_formatter.rb45
-rw-r--r--qa/qa/specs/helpers/rspec.rb4
-rw-r--r--qa/qa/support/allure_metadata_formatter.rb37
-rw-r--r--qa/qa/support/api.rb33
-rw-r--r--qa/qa/support/repeater.rb26
-rw-r--r--qa/qa/support/retrier.rb20
-rw-r--r--qa/spec/page/logging_spec.rb1
-rw-r--r--qa/spec/qa_deprecation_toolkit_env.rb22
-rw-r--r--qa/spec/resource/base_spec.rb78
-rw-r--r--qa/spec/runtime/api/request_spec.rb2
-rw-r--r--qa/spec/spec_helper.rb7
-rw-r--r--qa/spec/specs/allure_report_spec.rb93
-rw-r--r--qa/spec/specs/helpers/context_selector_spec.rb28
-rw-r--r--qa/spec/specs/helpers/quarantine_spec.rb28
-rw-r--r--qa/spec/support/allure_metadata_formatter_spec.rb46
-rw-r--r--qa/spec/support/matchers/eventually_matcher.rb19
-rw-r--r--qa/spec/support/retrier_spec.rb10
87 files changed, 2738 insertions, 574 deletions
diff --git a/qa/Gemfile b/qa/Gemfile
index ff2074b6191..3ba1244d17e 100644
--- a/qa/Gemfile
+++ b/qa/Gemfile
@@ -5,14 +5,14 @@ source 'https://rubygems.org'
gem 'gitlab-qa'
gem 'activesupport', '~> 6.1.3.2' # This should stay in sync with the root's Gemfile
gem 'allure-rspec', '~> 2.14.1'
-gem 'capybara', '~> 3.29.0'
+gem 'capybara', '~> 3.35.0'
gem 'capybara-screenshot', '~> 1.0.23'
gem 'rake', '~> 12.3.3'
-gem 'rspec', '~> 3.7'
+gem 'rspec', '~> 3.10'
gem 'selenium-webdriver', '~> 4.0.0.beta4'
gem 'airborne', '~> 0.3.4'
gem 'rest-client', '~> 2.1.0'
-gem 'nokogiri', '~> 1.11.1'
+gem 'nokogiri', '~> 1.11.7'
gem 'rspec-retry', '~> 0.6.1'
gem 'rspec_junit_formatter', '~> 0.4.1'
gem 'faker', '~> 1.6', '>= 1.6.6'
@@ -22,12 +22,14 @@ gem 'rotp', '~> 3.1.0'
gem 'timecop', '~> 0.9.1'
gem 'parallel', '~> 1.19'
gem 'rspec-parameterized', '~> 0.4.2'
-gem "octokit", "~> 4.21"
-gem "webdrivers", "~> 4.6"
+gem 'octokit', '~> 4.21'
+gem 'webdrivers', '~> 4.6'
gem 'chemlab', '~> 0.7'
gem 'chemlab-library-www-gitlab-com', '~> 0.1'
+gem 'deprecation_toolkit', '~> 1.5.1', require: false
+
group :development do
gem 'pry-byebug', '~> 3.5.1', platform: :mri
gem "ruby-debug-ide", "~> 0.7.0"
diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock
index 47dd5ac118e..66b635868f8 100644
--- a/qa/Gemfile.lock
+++ b/qa/Gemfile.lock
@@ -30,13 +30,13 @@ GEM
ast (2.4.1)
binding_ninja (0.2.3)
byebug (9.1.0)
- capybara (3.29.0)
+ capybara (3.35.3)
addressable
mini_mime (>= 0.1.3)
nokogiri (~> 1.8)
rack (>= 1.6.0)
rack-test (>= 0.6.3)
- regexp_parser (~> 1.5)
+ regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2)
capybara-screenshot (1.0.23)
capybara (>= 1.0, < 4)
@@ -56,6 +56,8 @@ GEM
adamantium (~> 0.2.0)
equalizer (~> 0.0.9)
concurrent-ruby (1.1.9)
+ deprecation_toolkit (1.5.1)
+ activesupport (>= 4.2)
diff-lcs (1.3)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
@@ -98,12 +100,12 @@ GEM
mime-types (3.3.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2021.0704)
- mini_mime (1.0.2)
- mini_portile2 (2.5.0)
+ mini_mime (1.1.0)
+ mini_portile2 (2.5.3)
minitest (5.14.4)
multipart-post (2.1.1)
netrc (0.11.0)
- nokogiri (1.11.1)
+ nokogiri (1.11.7)
mini_portile2 (~> 2.5.0)
racc (~> 1.4)
octokit (4.21.0)
@@ -141,18 +143,18 @@ GEM
netrc (~> 0.8)
rexml (3.2.5)
rotp (3.1.0)
- rspec (3.9.0)
- rspec-core (~> 3.9.0)
- rspec-expectations (~> 3.9.0)
- rspec-mocks (~> 3.9.0)
- rspec-core (3.9.3)
- rspec-support (~> 3.9.3)
- rspec-expectations (3.9.1)
+ rspec (3.10.0)
+ rspec-core (~> 3.10.0)
+ rspec-expectations (~> 3.10.0)
+ rspec-mocks (~> 3.10.0)
+ rspec-core (3.10.1)
+ rspec-support (~> 3.10.0)
+ rspec-expectations (3.10.1)
diff-lcs (>= 1.2.0, < 2.0)
- rspec-support (~> 3.9.0)
- rspec-mocks (3.9.1)
+ rspec-support (~> 3.10.0)
+ rspec-mocks (3.10.2)
diff-lcs (>= 1.2.0, < 2.0)
- rspec-support (~> 3.9.0)
+ rspec-support (~> 3.10.0)
rspec-parameterized (0.4.2)
binding_ninja (>= 0.2.3)
parser
@@ -161,7 +163,7 @@ GEM
unparser
rspec-retry (0.6.1)
rspec-core (> 3.3)
- rspec-support (3.9.4)
+ rspec-support (3.10.2)
rspec_junit_formatter (0.4.1)
rspec-core (>= 2, < 4, != 2.12.0)
ruby-debug-ide (0.7.2)
@@ -211,14 +213,15 @@ DEPENDENCIES
activesupport (~> 6.1.3.2)
airborne (~> 0.3.4)
allure-rspec (~> 2.14.1)
- capybara (~> 3.29.0)
+ capybara (~> 3.35.0)
capybara-screenshot (~> 1.0.23)
chemlab (~> 0.7)
chemlab-library-www-gitlab-com (~> 0.1)
+ deprecation_toolkit (~> 1.5.1)
faker (~> 1.6, >= 1.6.6)
gitlab-qa
knapsack (~> 1.17)
- nokogiri (~> 1.11.1)
+ nokogiri (~> 1.11.7)
octokit (~> 4.21)
parallel (~> 1.19)
parallel_tests (~> 2.29)
@@ -226,7 +229,7 @@ DEPENDENCIES
rake (~> 12.3.3)
rest-client (~> 2.1.0)
rotp (~> 3.1.0)
- rspec (~> 3.7)
+ rspec (~> 3.10)
rspec-parameterized (~> 0.4.2)
rspec-retry (~> 0.6.1)
rspec_junit_formatter (~> 0.4.1)
diff --git a/qa/chemlab-library-gitlab.gemspec b/qa/chemlab-library-gitlab.gemspec
new file mode 100644
index 00000000000..908aad01850
--- /dev/null
+++ b/qa/chemlab-library-gitlab.gemspec
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+$:.unshift(File.expand_path('lib', __dir__))
+
+Gem::Specification.new do |spec|
+ spec.name = 'chemlab-library-gitlab'
+ spec.version = '0.1.1'
+ spec.authors = ['GitLab Quality']
+ spec.email = ['quality@gitlab.com']
+
+ spec.required_ruby_version = '>= 2.5' # rubocop:disable Gemspec/RequiredRubyVersion
+
+ spec.summary = 'Chemlab Page Libraries for GitLab'
+ spec.homepage = 'https://gitlab.com/'
+ spec.license = 'MIT'
+
+ spec.files = `git ls-files -- lib/*`.split("\n")
+
+ spec.require_paths = ['lib']
+
+ spec.add_runtime_dependency 'chemlab', '~> 0.7'
+end
diff --git a/qa/lib/gitlab.rb b/qa/lib/gitlab.rb
new file mode 100644
index 00000000000..d0d1d535114
--- /dev/null
+++ b/qa/lib/gitlab.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+# Chemlab Page Libraries for GitLab
+module Gitlab
+ module Page
+ module Main
+ autoload :Login, 'gitlab/page/main/login'
+ end
+
+ module Subscriptions
+ autoload :New, 'gitlab/page/subscriptions/new'
+ end
+
+ module Group
+ module Settings
+ autoload :Billing, 'gitlab/page/group/settings/billing'
+ end
+ end
+ end
+end
diff --git a/qa/lib/gitlab/page/group/settings/billing.rb b/qa/lib/gitlab/page/group/settings/billing.rb
new file mode 100644
index 00000000000..24d327502f8
--- /dev/null
+++ b/qa/lib/gitlab/page/group/settings/billing.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Page
+ module Group
+ module Settings
+ class Billing < Chemlab::Page
+ # TODO: Supplant with data-qa-selectors
+ h4 :billing_plan_header, css: 'div.billing-plan-header h4'
+
+ link :start_your_free_trial
+
+ link :upgrade_to_premium, css: '[data-testid="plan-card-premium"] a.billing-cta-purchase-new'
+ link :upgrade_to_ultimate, css: '[data-testid="plan-card-ultimate"] a.billing-cta-purchase-new'
+ end
+ end
+ end
+ end
+end
diff --git a/qa/lib/gitlab/page/group/settings/billing.stub.rb b/qa/lib/gitlab/page/group/settings/billing.stub.rb
new file mode 100644
index 00000000000..64176af794a
--- /dev/null
+++ b/qa/lib/gitlab/page/group/settings/billing.stub.rb
@@ -0,0 +1,107 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Page
+ module Group
+ module Settings
+ module Billing
+ # @note Defined as +h4 :billing_plan_header+
+ # @return [String] The text content or value of +billing_plan_header+
+ def billing_plan_header
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Group::Settings::Billing.perform do |billing|
+ # expect(billing.billing_plan_header_element).to exist
+ # end
+ # @return [Watir::H4] The raw +H4+ element
+ def billing_plan_header_element
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Group::Settings::Billing.perform do |billing|
+ # expect(billing).to be_billing_plan_header
+ # end
+ # @return [Boolean] true if the +billing_plan_header+ element is present on the page
+ def billing_plan_header?
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @note Defined as +link :start_your_free_trial+
+ # Clicks +start_your_free_trial+
+ def start_your_free_trial
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Group::Settings::Billing.perform do |billing|
+ # expect(billing.start_your_free_trial_element).to exist
+ # end
+ # @return [Watir::Link] The raw +Link+ element
+ def start_your_free_trial_element
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Group::Settings::Billing.perform do |billing|
+ # expect(billing).to be_start_your_free_trial
+ # end
+ # @return [Boolean] true if the +start_your_free_trial+ element is present on the page
+ def start_your_free_trial?
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @note Defined as +link :upgrade_to_premium+
+ # Clicks +upgrade_to_premium+
+ def upgrade_to_premium
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Group::Settings::Billing.perform do |billing|
+ # expect(billing.upgrade_to_premium_element).to exist
+ # end
+ # @return [Watir::Link] The raw +Link+ element
+ def upgrade_to_premium_element
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Group::Settings::Billing.perform do |billing|
+ # expect(billing).to be_upgrade_to_premium
+ # end
+ # @return [Boolean] true if the +upgrade_to_premium+ element is present on the page
+ def upgrade_to_premium?
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @note Defined as +link :upgrade_to_ultimate+
+ # Clicks +upgrade_to_ultimate+
+ def upgrade_to_ultimate
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Group::Settings::Billing.perform do |billing|
+ # expect(billing.upgrade_to_ultimate_element).to exist
+ # end
+ # @return [Watir::Link] The raw +Link+ element
+ def upgrade_to_ultimate_element
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Group::Settings::Billing.perform do |billing|
+ # expect(billing).to be_upgrade_to_ultimate
+ # end
+ # @return [Boolean] true if the +upgrade_to_ultimate+ element is present on the page
+ def upgrade_to_ultimate?
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/lib/gitlab/page/main/login.rb b/qa/lib/gitlab/page/main/login.rb
new file mode 100644
index 00000000000..9f20a040550
--- /dev/null
+++ b/qa/lib/gitlab/page/main/login.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Page
+ module Main
+ class Login < Chemlab::Page
+ path '/users/sign_in'
+
+ text_field :login_field
+ text_field :password_field
+ button :sign_in_button
+
+ def sign_in_as(username:, password:)
+ self.login_field = username
+ self.password_field = password
+
+ sign_in_button
+ end
+ end
+ end
+ end
+end
diff --git a/qa/lib/gitlab/page/main/login.stub.rb b/qa/lib/gitlab/page/main/login.stub.rb
new file mode 100644
index 00000000000..a4cef291616
--- /dev/null
+++ b/qa/lib/gitlab/page/main/login.stub.rb
@@ -0,0 +1,101 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Page
+ module Main
+ module Login
+ # @note Defined as +text_field :login_field+
+ # @return [String] The text content or value of +login_field+
+ def login_field
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # Set the value of login_field
+ # @example
+ # Gitlab::Page::Main::Login.perform do |login|
+ # login.login_field = 'value'
+ # end
+ # @param value [String] The value to set.
+ def login_field=(value)
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Main::Login.perform do |login|
+ # expect(login.login_field_element).to exist
+ # end
+ # @return [Watir::TextField] The raw +TextField+ element
+ def login_field_element
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Main::Login.perform do |login|
+ # expect(login).to be_login_field
+ # end
+ # @return [Boolean] true if the +login_field+ element is present on the page
+ def login_field?
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @note Defined as +text_field :password_field+
+ # @return [String] The text content or value of +password_field+
+ def password_field
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # Set the value of password_field
+ # @example
+ # Gitlab::Page::Main::Login.perform do |login|
+ # login.password_field = 'value'
+ # end
+ # @param value [String] The value to set.
+ def password_field=(value)
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Main::Login.perform do |login|
+ # expect(login.password_field_element).to exist
+ # end
+ # @return [Watir::TextField] The raw +TextField+ element
+ def password_field_element
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Main::Login.perform do |login|
+ # expect(login).to be_password_field
+ # end
+ # @return [Boolean] true if the +password_field+ element is present on the page
+ def password_field?
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @note Defined as +button :sign_in_button+
+ # Clicks +sign_in_button+
+ def sign_in_button
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Main::Login.perform do |login|
+ # expect(login.sign_in_button_element).to exist
+ # end
+ # @return [Watir::Button] The raw +Button+ element
+ def sign_in_button_element
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Main::Login.perform do |login|
+ # expect(login).to be_sign_in_button
+ # end
+ # @return [Boolean] true if the +sign_in_button+ element is present on the page
+ def sign_in_button?
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+ end
+ end
+ end
+end
diff --git a/qa/lib/gitlab/page/subscriptions/new.rb b/qa/lib/gitlab/page/subscriptions/new.rb
new file mode 100644
index 00000000000..4c0e5446444
--- /dev/null
+++ b/qa/lib/gitlab/page/subscriptions/new.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Page
+ module Subscriptions
+ class New < Chemlab::Page
+ path '/subscriptions/new'
+
+ # Subscription Details
+ select :plan_name
+ select :group_name
+ text_field :number_of_users
+ button :continue_to_billing, text: /Continue to billing/
+
+ # Billing address
+ select :country
+ text_field :street_address_1
+ text_field :street_address_2
+ text_field :city
+ select :state
+ text_field :zip_code
+ button :continue_to_payment, text: /Continue to payment/
+
+ # Payment method
+ # TODO: Revisit when https://gitlab.com/gitlab-org/quality/chemlab/-/issues/6 is closed
+ iframe :payment_form, id: 'z_hppm_iframe'
+
+ text_field(:name_on_card) { payment_form_element.text_field(id: 'input-creditCardHolderName') }
+ text_field(:card_number) { payment_form_element.text_field(id: 'input-creditCardNumber') }
+ select(:expiration_month) { payment_form_element.select(id: 'input-creditCardExpirationMonth') }
+ select(:expiration_year) { payment_form_element.select(id: 'input-creditCardExpirationYear') }
+ text_field(:cvv) { payment_form_element.text_field(id: 'input-cardSecurityCode') }
+ link(:review_your_order) { payment_form_element.link(text: /Review your order/) }
+ # ENDTODO
+
+ # Confirmation
+ button :confirm_purchase, text: /Confirm purchase/
+ end
+ end
+ end
+end
diff --git a/qa/lib/gitlab/page/subscriptions/new.stub.rb b/qa/lib/gitlab/page/subscriptions/new.stub.rb
new file mode 100644
index 00000000000..93680ab95e0
--- /dev/null
+++ b/qa/lib/gitlab/page/subscriptions/new.stub.rb
@@ -0,0 +1,545 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Page
+ module Subscriptions
+ module New
+ # @note Defined as +select :plan_name+
+ # @return [String] The text content or value of +plan_name+
+ def plan_name
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Subscriptions::New.perform do |new|
+ # expect(new.plan_name_element).to exist
+ # end
+ # @return [Watir::Select] The raw +Select+ element
+ def plan_name_element
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Subscriptions::New.perform do |new|
+ # expect(new).to be_plan_name
+ # end
+ # @return [Boolean] true if the +plan_name+ element is present on the page
+ def plan_name?
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @note Defined as +select :group_name+
+ # @return [String] The text content or value of +group_name+
+ def group_name
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Subscriptions::New.perform do |new|
+ # expect(new.group_name_element).to exist
+ # end
+ # @return [Watir::Select] The raw +Select+ element
+ def group_name_element
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Subscriptions::New.perform do |new|
+ # expect(new).to be_group_name
+ # end
+ # @return [Boolean] true if the +group_name+ element is present on the page
+ def group_name?
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @note Defined as +text_field :number_of_users+
+ # @return [String] The text content or value of +number_of_users+
+ def number_of_users
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # Set the value of number_of_users
+ # @example
+ # Gitlab::Page::Subscriptions::New.perform do |new|
+ # new.number_of_users = 'value'
+ # end
+ # @param value [String] The value to set.
+ def number_of_users=(value)
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Subscriptions::New.perform do |new|
+ # expect(new.number_of_users_element).to exist
+ # end
+ # @return [Watir::TextField] The raw +TextField+ element
+ def number_of_users_element
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Subscriptions::New.perform do |new|
+ # expect(new).to be_number_of_users
+ # end
+ # @return [Boolean] true if the +number_of_users+ element is present on the page
+ def number_of_users?
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @note Defined as +button :continue_to_billing+
+ # Clicks +continue_to_billing+
+ def continue_to_billing
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Subscriptions::New.perform do |new|
+ # expect(new.continue_to_billing_element).to exist
+ # end
+ # @return [Watir::Button] The raw +Button+ element
+ def continue_to_billing_element
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Subscriptions::New.perform do |new|
+ # expect(new).to be_continue_to_billing
+ # end
+ # @return [Boolean] true if the +continue_to_billing+ element is present on the page
+ def continue_to_billing?
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @note Defined as +select :country+
+ # @return [String] The text content or value of +country+
+ def country
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Subscriptions::New.perform do |new|
+ # expect(new.country_element).to exist
+ # end
+ # @return [Watir::Select] The raw +Select+ element
+ def country_element
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Subscriptions::New.perform do |new|
+ # expect(new).to be_country
+ # end
+ # @return [Boolean] true if the +country+ element is present on the page
+ def country?
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @note Defined as +text_field :street_address_1+
+ # @return [String] The text content or value of +street_address_1+
+ def street_address_1
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # Set the value of street_address_1
+ # @example
+ # Gitlab::Page::Subscriptions::New.perform do |new|
+ # new.street_address_1 = 'value'
+ # end
+ # @param value [String] The value to set.
+ def street_address_1=(value)
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Subscriptions::New.perform do |new|
+ # expect(new.street_address_1_element).to exist
+ # end
+ # @return [Watir::TextField] The raw +TextField+ element
+ def street_address_1_element
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Subscriptions::New.perform do |new|
+ # expect(new).to be_street_address_1
+ # end
+ # @return [Boolean] true if the +street_address_1+ element is present on the page
+ def street_address_1?
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @note Defined as +text_field :street_address_2+
+ # @return [String] The text content or value of +street_address_2+
+ def street_address_2
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # Set the value of street_address_2
+ # @example
+ # Gitlab::Page::Subscriptions::New.perform do |new|
+ # new.street_address_2 = 'value'
+ # end
+ # @param value [String] The value to set.
+ def street_address_2=(value)
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Subscriptions::New.perform do |new|
+ # expect(new.street_address_2_element).to exist
+ # end
+ # @return [Watir::TextField] The raw +TextField+ element
+ def street_address_2_element
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Subscriptions::New.perform do |new|
+ # expect(new).to be_street_address_2
+ # end
+ # @return [Boolean] true if the +street_address_2+ element is present on the page
+ def street_address_2?
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @note Defined as +text_field :city+
+ # @return [String] The text content or value of +city+
+ def city
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # Set the value of city
+ # @example
+ # Gitlab::Page::Subscriptions::New.perform do |new|
+ # new.city = 'value'
+ # end
+ # @param value [String] The value to set.
+ def city=(value)
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Subscriptions::New.perform do |new|
+ # expect(new.city_element).to exist
+ # end
+ # @return [Watir::TextField] The raw +TextField+ element
+ def city_element
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Subscriptions::New.perform do |new|
+ # expect(new).to be_city
+ # end
+ # @return [Boolean] true if the +city+ element is present on the page
+ def city?
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @note Defined as +select :state+
+ # @return [String] The text content or value of +state+
+ def state
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Subscriptions::New.perform do |new|
+ # expect(new.state_element).to exist
+ # end
+ # @return [Watir::Select] The raw +Select+ element
+ def state_element
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Subscriptions::New.perform do |new|
+ # expect(new).to be_state
+ # end
+ # @return [Boolean] true if the +state+ element is present on the page
+ def state?
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @note Defined as +text_field :zip_code+
+ # @return [String] The text content or value of +zip_code+
+ def zip_code
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # Set the value of zip_code
+ # @example
+ # Gitlab::Page::Subscriptions::New.perform do |new|
+ # new.zip_code = 'value'
+ # end
+ # @param value [String] The value to set.
+ def zip_code=(value)
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Subscriptions::New.perform do |new|
+ # expect(new.zip_code_element).to exist
+ # end
+ # @return [Watir::TextField] The raw +TextField+ element
+ def zip_code_element
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Subscriptions::New.perform do |new|
+ # expect(new).to be_zip_code
+ # end
+ # @return [Boolean] true if the +zip_code+ element is present on the page
+ def zip_code?
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @note Defined as +button :continue_to_payment+
+ # Clicks +continue_to_payment+
+ def continue_to_payment
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Subscriptions::New.perform do |new|
+ # expect(new.continue_to_payment_element).to exist
+ # end
+ # @return [Watir::Button] The raw +Button+ element
+ def continue_to_payment_element
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Subscriptions::New.perform do |new|
+ # expect(new).to be_continue_to_payment
+ # end
+ # @return [Boolean] true if the +continue_to_payment+ element is present on the page
+ def continue_to_payment?
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @note Defined as +iframe :payment_form+
+ # @return [String] The text content or value of +payment_form+
+ def payment_form
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Subscriptions::New.perform do |new|
+ # expect(new.payment_form_element).to exist
+ # end
+ # @return [Watir::Iframe] The raw +Iframe+ element
+ def payment_form_element
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Subscriptions::New.perform do |new|
+ # expect(new).to be_payment_form
+ # end
+ # @return [Boolean] true if the +payment_form+ element is present on the page
+ def payment_form?
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @note Defined as +text_field :name_on_card+
+ # @return [String] The text content or value of +name_on_card+
+ def name_on_card
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # Set the value of name_on_card
+ # @example
+ # Gitlab::Page::Subscriptions::New.perform do |new|
+ # new.name_on_card = 'value'
+ # end
+ # @param value [String] The value to set.
+ def name_on_card=(value)
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Subscriptions::New.perform do |new|
+ # expect(new.name_on_card_element).to exist
+ # end
+ # @return [Watir::TextField] The raw +TextField+ element
+ def name_on_card_element
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Subscriptions::New.perform do |new|
+ # expect(new).to be_name_on_card
+ # end
+ # @return [Boolean] true if the +name_on_card+ element is present on the page
+ def name_on_card?
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @note Defined as +text_field :card_number+
+ # @return [String] The text content or value of +card_number+
+ def card_number
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # Set the value of card_number
+ # @example
+ # Gitlab::Page::Subscriptions::New.perform do |new|
+ # new.card_number = 'value'
+ # end
+ # @param value [String] The value to set.
+ def card_number=(value)
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Subscriptions::New.perform do |new|
+ # expect(new.card_number_element).to exist
+ # end
+ # @return [Watir::TextField] The raw +TextField+ element
+ def card_number_element
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Subscriptions::New.perform do |new|
+ # expect(new).to be_card_number
+ # end
+ # @return [Boolean] true if the +card_number+ element is present on the page
+ def card_number?
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @note Defined as +select :expiration_month+
+ # @return [String] The text content or value of +expiration_month+
+ def expiration_month
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Subscriptions::New.perform do |new|
+ # expect(new.expiration_month_element).to exist
+ # end
+ # @return [Watir::Select] The raw +Select+ element
+ def expiration_month_element
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Subscriptions::New.perform do |new|
+ # expect(new).to be_expiration_month
+ # end
+ # @return [Boolean] true if the +expiration_month+ element is present on the page
+ def expiration_month?
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @note Defined as +select :expiration_year+
+ # @return [String] The text content or value of +expiration_year+
+ def expiration_year
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Subscriptions::New.perform do |new|
+ # expect(new.expiration_year_element).to exist
+ # end
+ # @return [Watir::Select] The raw +Select+ element
+ def expiration_year_element
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Subscriptions::New.perform do |new|
+ # expect(new).to be_expiration_year
+ # end
+ # @return [Boolean] true if the +expiration_year+ element is present on the page
+ def expiration_year?
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @note Defined as +text_field :cvv+
+ # @return [String] The text content or value of +cvv+
+ def cvv
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # Set the value of cvv
+ # @example
+ # Gitlab::Page::Subscriptions::New.perform do |new|
+ # new.cvv = 'value'
+ # end
+ # @param value [String] The value to set.
+ def cvv=(value)
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Subscriptions::New.perform do |new|
+ # expect(new.cvv_element).to exist
+ # end
+ # @return [Watir::TextField] The raw +TextField+ element
+ def cvv_element
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Subscriptions::New.perform do |new|
+ # expect(new).to be_cvv
+ # end
+ # @return [Boolean] true if the +cvv+ element is present on the page
+ def cvv?
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @note Defined as +link :review_your_order+
+ # Clicks +review_your_order+
+ def review_your_order
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Subscriptions::New.perform do |new|
+ # expect(new.review_your_order_element).to exist
+ # end
+ # @return [Watir::Link] The raw +Link+ element
+ def review_your_order_element
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Subscriptions::New.perform do |new|
+ # expect(new).to be_review_your_order
+ # end
+ # @return [Boolean] true if the +review_your_order+ element is present on the page
+ def review_your_order?
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @note Defined as +button :confirm_purchase+
+ # Clicks +confirm_purchase+
+ def confirm_purchase
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Subscriptions::New.perform do |new|
+ # expect(new.confirm_purchase_element).to exist
+ # end
+ # @return [Watir::Button] The raw +Button+ element
+ def confirm_purchase_element
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Subscriptions::New.perform do |new|
+ # expect(new).to be_confirm_purchase
+ # end
+ # @return [Boolean] true if the +confirm_purchase+ element is present on the page
+ def confirm_purchase?
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa.rb b/qa/qa.rb
index 566effb3cd9..965cc88e50c 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -8,6 +8,8 @@ require_relative '../lib/gitlab'
require_relative '../lib/gitlab/utils'
require_relative '../config/initializers/0_inject_enterprise_edition_module'
+require_relative 'lib/gitlab'
+
require 'chemlab'
module QA
@@ -71,6 +73,7 @@ module QA
autoload :GroupBase, 'qa/resource/group_base'
autoload :Sandbox, 'qa/resource/sandbox'
autoload :Group, 'qa/resource/group'
+ autoload :BulkImportGroup, 'qa/resource/bulk_import_group'
autoload :Issue, 'qa/resource/issue'
autoload :ProjectIssueNote, 'qa/resource/project_issue_note'
autoload :Project, 'qa/resource/project'
@@ -236,6 +239,7 @@ module QA
autoload :Menu, 'qa/page/group/menu'
autoload :Members, 'qa/page/group/members'
autoload :BulkImport, 'qa/page/group/bulk_import'
+ autoload :DependencyProxy, 'qa/page/group/dependency_proxy'
module Milestone
autoload :Index, 'qa/page/group/milestone/index'
@@ -385,7 +389,6 @@ module QA
module Deployments
module Environments
autoload :Index, 'qa/page/project/deployments/environments/index'
- autoload :Show, 'qa/page/project/deployments/environments/show'
end
end
@@ -424,6 +427,10 @@ module QA
autoload :Show, 'qa/page/project/snippet/show'
autoload :Index, 'qa/page/project/snippet/index'
end
+
+ module Secure
+ autoload :ConfigurationForm, 'qa/page/project/secure/configuration_form'
+ end
end
module Profile
@@ -538,6 +545,7 @@ module QA
autoload :AccessTokens, 'qa/page/component/access_tokens'
autoload :CommitModal, 'qa/page/component/commit_modal'
autoload :VisibilitySetting, 'qa/page/component/visibility_setting'
+ autoload :ContentEditor, 'qa/page/component/content_editor'
module Import
autoload :Gitlab, 'qa/page/component/import/gitlab'
@@ -627,7 +635,9 @@ module QA
module Helpers
autoload :ContextSelector, 'qa/specs/helpers/context_selector'
+ autoload :ContextFormatter, 'qa/specs/helpers/context_formatter'
autoload :Quarantine, 'qa/specs/helpers/quarantine'
+ autoload :QuarantineFormatter, 'qa/specs/helpers/quarantine_formatter'
autoload :RSpec, 'qa/specs/helpers/rspec'
end
end
@@ -675,6 +685,7 @@ module QA
autoload :WaitForRequests, 'qa/support/wait_for_requests'
autoload :OTP, 'qa/support/otp'
autoload :SSH, 'qa/support/ssh'
+ autoload :AllureMetadataFormatter, 'qa/support/allure_metadata_formatter.rb'
end
end
diff --git a/qa/qa/fixtures/export.tar.gz b/qa/qa/fixtures/export.tar.gz
index 08e4f0c9c43..8d27b6816ea 100644
--- a/qa/qa/fixtures/export.tar.gz
+++ b/qa/qa/fixtures/export.tar.gz
Binary files differ
diff --git a/qa/qa/page/base.rb b/qa/qa/page/base.rb
index 6b54d8ab1ac..9debdc1d4dd 100644
--- a/qa/qa/page/base.rb
+++ b/qa/qa/page/base.rb
@@ -21,7 +21,7 @@ module QA
end
def to_s
- <<~MSG.strip % { page: @page_class }
+ format(<<~MSG.strip, page: @page_class)
%{page} has no required elements.
See https://docs.gitlab.com/ee/development/testing_guide/end_to_end/dynamic_element_validation.html#required-elements
MSG
@@ -108,7 +108,7 @@ module QA
wait_for_requests(skip_finished_loading_check: skip_finished_loading_check)
element_selector = element_selector_css(name, reject_capybara_query_keywords(kwargs))
- find(element_selector, only_capybara_query_keywords(kwargs))
+ find(element_selector, **only_capybara_query_keywords(kwargs))
end
def only_capybara_query_keywords(kwargs)
@@ -232,11 +232,11 @@ module QA
visible = kwargs.delete(:visible)
visible = visible.nil? && true
- try_find_element = ->(wait) do
+ try_find_element = lambda do |wait|
if disabled.nil?
has_css?(element_selector_css(name, kwargs), text: text, wait: wait, class: klass, visible: visible)
else
- find_element(name, original_kwargs).disabled? == disabled
+ find_element(name, **original_kwargs).disabled? == disabled
end
end
@@ -322,13 +322,13 @@ module QA
# It would be ideal if we could detect when the animation is complete
# but in some cases there's nothing we can easily access via capybara
# so instead we wait for the element, and then we wait a little longer
- raise ElementNotFound, %Q(Couldn't find element named "#{name}") unless has_element?(name)
+ raise ElementNotFound, %(Couldn't find element named "#{name}") unless has_element?(name)
sleep 1
end
def within_element(name, **kwargs)
- wait_for_requests
+ wait_for_requests(skip_finished_loading_check: kwargs.delete(:skip_finished_loading_check))
text = kwargs.delete(:text)
page.within(element_selector_css(name, kwargs), text: text) do
@@ -386,9 +386,7 @@ module QA
end
def self.errors
- if views.empty?
- return ["Page class does not have views / elements defined!"]
- end
+ return ["Page class does not have views / elements defined!"] if views.empty?
views.flat_map(&:errors)
end
diff --git a/qa/qa/page/component/content_editor.rb b/qa/qa/page/component/content_editor.rb
new file mode 100644
index 00000000000..b3a42634fe7
--- /dev/null
+++ b/qa/qa/page/component/content_editor.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+module QA
+ module Page
+ module Component
+ module ContentEditor
+ extend QA::Page::PageConcern
+
+ def self.included(base)
+ super
+
+ base.view 'app/assets/javascripts/content_editor/components/content_editor.vue' do
+ element :content_editor_container
+ end
+
+ base.view 'app/assets/javascripts/content_editor/components/toolbar_text_style_dropdown.vue' do
+ element :text_style_dropdown
+ element :text_style_menu_item
+ end
+
+ base.view 'app/assets/javascripts/content_editor/components/toolbar_image_button.vue' do
+ element :file_upload_field
+ end
+ end
+
+ def add_heading(heading, text)
+ within_element(:content_editor_container) do
+ text_area.set(text)
+ # wait for text style option to become active after typing
+ has_active_element?(:text_style_dropdown, wait: 1)
+ click_element(:text_style_dropdown)
+ within_element(:text_style_dropdown) do
+ click_element(:text_style_menu_item, text_style: heading)
+ end
+ end
+ end
+
+ def upload_image(image_path)
+ within_element(:content_editor_container) do
+ # add image on a new line
+ text_area.send_keys(:return)
+ find_element(:file_upload_field, visible: false).send_keys(image_path)
+ end
+ end
+
+ private
+
+ def text_area
+ find('[contenteditable="true"]', visible: false)
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/component/import/gitlab.rb b/qa/qa/page/component/import/gitlab.rb
index 2fd2a45b399..5831a713ae5 100644
--- a/qa/qa/page/component/import/gitlab.rb
+++ b/qa/qa/page/component/import/gitlab.rb
@@ -5,6 +5,8 @@ module QA
module Component
module Import
module Gitlab
+ extend QA::Page::PageConcern
+
def self.included(base)
super
@@ -30,7 +32,7 @@ module QA
click_element(:import_project_button)
wait_until(reload: false) do
- has_notice?("The project was successfully imported.")
+ has_notice?("The project was successfully imported.") || has_element?(:project_name_content)
end
end
end
diff --git a/qa/qa/page/component/invite_members_modal.rb b/qa/qa/page/component/invite_members_modal.rb
index 7cec4588af5..fecd61fb410 100644
--- a/qa/qa/page/component/invite_members_modal.rb
+++ b/qa/qa/page/component/invite_members_modal.rb
@@ -40,18 +40,20 @@ module QA
click_element :invite_a_group_button
end
- def add_member(username, access_level = Resource::Members::AccessLevel::DEVELOPER)
+ def add_member(username, access_level = 'Developer')
open_invite_members_modal
within_element(:invite_members_modal_content) do
- fill_element :access_level_dropdown, with: access_level
-
fill_element :members_token_select_input, username
-
Support::WaitForRequests.wait_for_requests
-
click_button username
+ # Guest option is selected by default, skipping these steps if desired option is 'Guest'
+ unless access_level == 'Guest'
+ click_element :access_level_dropdown
+ click_button access_level
+ end
+
click_element :invite_button
end
diff --git a/qa/qa/page/component/wiki.rb b/qa/qa/page/component/wiki.rb
index 92eb25af247..c3db1d6c885 100644
--- a/qa/qa/page/component/wiki.rb
+++ b/qa/qa/page/component/wiki.rb
@@ -68,6 +68,18 @@ module QA
def has_no_page?
has_element?(:create_first_page_link)
end
+
+ def has_heading?(heading_type, text)
+ within_element(:wiki_page_content) do
+ has_css?(heading_type, text: text)
+ end
+ end
+
+ def has_image?(image_file_name)
+ within_element(:wiki_page_content) do
+ has_css?("img[src$='#{image_file_name}']")
+ end
+ end
end
end
end
diff --git a/qa/qa/page/component/wiki_page_form.rb b/qa/qa/page/component/wiki_page_form.rb
index bb22b7da003..6b7452b0e0f 100644
--- a/qa/qa/page/component/wiki_page_form.rb
+++ b/qa/qa/page/component/wiki_page_form.rb
@@ -14,6 +14,7 @@ module QA
element :wiki_content_textarea
element :wiki_message_textbox
element :wiki_submit_button
+ element :try_new_editor_container
end
base.view 'app/assets/javascripts/pages/shared/wikis/components/delete_wiki_modal.vue' do
@@ -41,6 +42,12 @@ module QA
click_element(:delete_button, Page::Modal::DeleteWiki)
Page::Modal::DeleteWiki.perform(&:confirm_deletion)
end
+
+ def use_new_editor
+ within_element(:try_new_editor_container) do
+ click_button('Use the new editor')
+ end
+ end
end
end
end
diff --git a/qa/qa/page/dashboard/projects.rb b/qa/qa/page/dashboard/projects.rb
index 8be11550233..c0108d85365 100644
--- a/qa/qa/page/dashboard/projects.rb
+++ b/qa/qa/page/dashboard/projects.rb
@@ -13,6 +13,10 @@ module QA
element :user_role_content
end
+ view 'app/views/dashboard/_projects_head.html.haml' do
+ element :new_project_button
+ end
+
def has_project_with_access_role?(project_name, access_role)
within_element(:project_content, text: project_name) do
has_element?(:user_role_content, text: access_role)
@@ -25,6 +29,10 @@ module QA
find_link(text: name).click
end
+ def click_new_project_button
+ click_element(:new_project_button, Page::Project::New)
+ end
+
def self.path
'/'
end
diff --git a/qa/qa/page/group/bulk_import.rb b/qa/qa/page/group/bulk_import.rb
index a0511c9a16c..9ba80abf21c 100644
--- a/qa/qa/page/group/bulk_import.rb
+++ b/qa/qa/page/group/bulk_import.rb
@@ -6,13 +6,13 @@ module QA
class BulkImport < Page::Base
view "app/assets/javascripts/import_entities/import_groups/components/import_table.vue" do
element :import_table
+ element :import_item
+ element :import_group_button
+ element :import_status_indicator
end
- view "app/assets/javascripts/import_entities/import_groups/components/import_table_row.vue" do
- element :import_item
+ view "app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue" do
element :target_group_dropdown_item
- element :import_status_indicator
- element :import_group_button
end
view "app/assets/javascripts/import_entities/components/group_dropdown.vue" do
diff --git a/qa/qa/page/group/dependency_proxy.rb b/qa/qa/page/group/dependency_proxy.rb
new file mode 100644
index 00000000000..f637c79cffc
--- /dev/null
+++ b/qa/qa/page/group/dependency_proxy.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module QA
+ module Page
+ module Group
+ class DependencyProxy < QA::Page::Base
+ view 'app/views/groups/dependency_proxies/show.html.haml' do
+ element :dependency_proxy_setting_toggle
+ end
+
+ view 'app/views/groups/dependency_proxies/_url.html.haml' do
+ element :dependency_proxy_count
+ end
+
+ def has_dependency_proxy_enabled?
+ toggle = find_element(:dependency_proxy_setting_toggle)
+ toggle[:class].include?('is-checked')
+ end
+
+ def has_blob_count?(blob_text)
+ has_element?(:dependency_proxy_count, text: blob_text)
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/group/members.rb b/qa/qa/page/group/members.rb
index b526a4488b2..ccc901932f4 100644
--- a/qa/qa/page/group/members.rb
+++ b/qa/qa/page/group/members.rb
@@ -7,7 +7,7 @@ module QA
include Page::Component::InviteMembersModal
include Page::Component::UsersSelect
- view 'app/assets/javascripts/vue_shared/components/remove_member_modal.vue' do
+ view 'app/assets/javascripts/members/components/modals/remove_member_modal.vue' do
element :remove_member_modal_content
end
diff --git a/qa/qa/page/group/menu.rb b/qa/qa/page/group/menu.rb
index 338a135614d..c997598e25a 100644
--- a/qa/qa/page/group/menu.rb
+++ b/qa/qa/page/group/menu.rb
@@ -6,51 +6,32 @@ module QA
class Menu < Page::Base
include SubMenus::Common
- view 'app/views/layouts/nav/sidebar/_group_menus.html.haml' do
- element :general_settings_link
- element :group_issues_item
- element :group_members_item
- element :group_milestones_link
- element :group_settings
- element :group_information_link
- element :group_information_submenu
- end
-
- view 'app/views/groups/sidebar/_packages_settings.html.haml' do
- element :group_package_settings_link
- end
-
- view 'app/views/layouts/nav/sidebar/_analytics_links.html.haml' do
- element :analytics_link
- element :analytics_sidebar_submenu
- end
-
def click_group_members_item
- hover_element(:group_information_link) do
- within_submenu(:group_information_submenu) do
- click_element(:group_members_item)
+ hover_group_information do
+ within_submenu do
+ click_element(:sidebar_menu_item_link, menu_item: 'Members')
end
end
end
- def click_settings
- within_sidebar do
- click_element(:group_settings)
+ def click_subgroup_members_item
+ hover_subgroup_information do
+ within_submenu do
+ click_element(:sidebar_menu_item_link, menu_item: 'Members')
+ end
end
end
- def click_contribution_analytics_item
- hover_element(:analytics_link) do
- within_submenu(:analytics_sidebar_submenu) do
- click_element(:contribution_analytics_link)
- end
+ def click_settings
+ within_sidebar do
+ click_element(:sidebar_menu_link, menu_item: 'Settings')
end
end
def click_group_general_settings_item
- hover_element(:group_settings) do
- within_submenu(:group_sidebar_submenu) do
- click_element(:general_settings_link)
+ hover_group_settings do
+ within_submenu do
+ click_element(:sidebar_menu_item_link, menu_item: 'General')
end
end
end
@@ -58,16 +39,31 @@ module QA
def go_to_milestones
hover_issues do
within_submenu do
- click_element(:group_milestones_link)
+ click_element(:sidebar_menu_item_link, menu_item: 'Milestones')
end
end
end
def go_to_package_settings
- scroll_to_element(:group_settings)
- hover_element(:group_settings) do
- within_submenu(:group_sidebar_submenu) do
- click_element(:group_package_settings_link)
+ hover_group_settings do
+ within_submenu do
+ click_element(:sidebar_menu_item_link, menu_item: 'Packages & Registries')
+ end
+ end
+ end
+
+ def go_to_group_packages
+ hover_group_packages do
+ within_submenu do
+ click_element(:sidebar_menu_item_link, menu_item: 'Package Registry')
+ end
+ end
+ end
+
+ def go_to_dependency_proxy
+ hover_group_packages do
+ within_submenu do
+ click_element(:sidebar_menu_item_link, menu_item: 'Dependency Proxy')
end
end
end
@@ -76,8 +72,42 @@ module QA
def hover_issues
within_sidebar do
- scroll_to_element(:group_issues_item)
- find_element(:group_issues_item).hover
+ scroll_to_element(:sidebar_menu_link, menu_item: 'Issues')
+ find_element(:sidebar_menu_link, menu_item: 'Issues').hover
+
+ yield
+ end
+ end
+
+ def hover_group_information
+ within_sidebar do
+ find_element(:sidebar_menu_link, menu_item: 'Group information').hover
+
+ yield
+ end
+ end
+
+ def hover_subgroup_information
+ within_sidebar do
+ find_element(:sidebar_menu_link, menu_item: 'Subgroup information').hover
+
+ yield
+ end
+ end
+
+ def hover_group_packages
+ within_sidebar do
+ scroll_to_element(:sidebar_menu_link, menu_item: 'Packages & Registries')
+ find_element(:sidebar_menu_link, menu_item: 'Packages & Registries').hover
+
+ yield
+ end
+ end
+
+ def hover_group_settings
+ within_sidebar do
+ scroll_to_element(:sidebar_menu_link, menu_item: 'Settings')
+ find_element(:sidebar_menu_link, menu_item: 'Settings').hover
yield
end
diff --git a/qa/qa/page/group/settings/billing.rb b/qa/qa/page/group/settings/billing.rb
deleted file mode 100644
index a83af47fc35..00000000000
--- a/qa/qa/page/group/settings/billing.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-# frozen_string_literal: true
-
-module QA
- module Page
- module Group
- module Settings
- class Billing < Chemlab::Page
- link :start_your_free_trial
- end
- end
- end
- end
-end
diff --git a/qa/qa/page/merge_request/show.rb b/qa/qa/page/merge_request/show.rb
index 5f52d48e9f6..afe88fc0cdc 100644
--- a/qa/qa/page/merge_request/show.rb
+++ b/qa/qa/page/merge_request/show.rb
@@ -202,7 +202,9 @@ module QA
def has_pipeline_status?(text)
# Pipelines can be slow, so we wait a bit longer than the usual 10 seconds
- has_element?(:merge_request_pipeline_info_content, text: text, wait: 60)
+ wait_until(sleep_interval: 5, reload: false) do
+ has_element?(:merge_request_pipeline_info_content, text: text, wait: 15 )
+ end
end
def has_title?(title)
@@ -236,7 +238,10 @@ module QA
end
def merged?
- # Revisit after merge page re-architect is done https://gitlab.com/gitlab-org/gitlab/-/issues/300042
+ # Reloads the page at this point to avoid the problem of the merge status failing to update
+ # That's the transient UX issue this test is checking for, so if the MR is merged but the UI still shows the
+ # status as unmerged, the test will fail.
+ # Revisit after merge page re-architect is done https://gitlab.com/groups/gitlab-org/-/epics/5598
# To remove page refresh logic if possible
retry_until(max_attempts: 3, reload: true) do
has_element?(:merged_status_content, text: 'The changes were merged into', wait: 20)
diff --git a/qa/qa/page/project/deployments/environments/show.rb b/qa/qa/page/project/deployments/environments/show.rb
deleted file mode 100644
index 48e4850d3be..00000000000
--- a/qa/qa/page/project/deployments/environments/show.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-# frozen_string_literal: true
-
-module QA
- module Page
- module Project
- module Deployments
- module Environments
- class Show < Page::Base
- view 'app/views/projects/environments/_external_url.html.haml' do
- element :view_deployment
- end
-
- def view_deployment(&block)
- new_window = window_opened_by { click_element(:view_deployment) }
-
- within_window(new_window, &block) if block
- end
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/page/project/import/github.rb b/qa/qa/page/project/import/github.rb
index 74bc4cec467..bb35c5eb17c 100644
--- a/qa/qa/page/project/import/github.rb
+++ b/qa/qa/page/project/import/github.rb
@@ -18,12 +18,17 @@ module QA
element :import_button
element :project_path_content
element :go_to_project_button
+ element :import_status_indicator
end
view "app/assets/javascripts/import_entities/components/group_dropdown.vue" do
element :target_namespace_selector_dropdown
end
+ # Add personal access token
+ #
+ # @param [String] personal_access_token
+ # @return [void]
def add_personal_access_token(personal_access_token)
# If for some reasons this process is retried, user cannot re-enter github token in the same group
# In this case skip this step and proceed to import project row
@@ -34,71 +39,50 @@ module QA
finished_loading?
end
- def import!(full_path, name)
- return if already_imported(full_path)
-
- choose_test_namespace(full_path)
- set_path(full_path, name)
- import_project(full_path)
-
- wait_for_success
- end
-
- # TODO: refactor to use 'go to project' button instead of generic main menu
- def go_to_project(name)
- Page::Main::Menu.perform(&:go_to_projects)
- Page::Dashboard::Projects.perform do |dashboard|
- dashboard.go_to_project(name)
- end
- end
-
- private
-
- def within_repo_path(full_path, &block)
- project_import_row = find_element(:project_import_row, text: full_path)
-
- within(project_import_row, &block)
- end
-
- def choose_test_namespace(full_path)
- within_repo_path(full_path) do
- within_element(:target_namespace_selector_dropdown) { click_button(class: 'dropdown-toggle') }
- click_element(:target_group_dropdown_item, group_name: Runtime::Namespace.path)
- end
- end
-
- def set_path(full_path, name)
- within_repo_path(full_path) do
- fill_element(:project_path_field, name)
- end
- end
-
- def import_project(full_path)
- within_repo_path(full_path) do
+ # Import project
+ #
+ # @param [String] source_project_name
+ # @param [String] target_group_path
+ # @return [void]
+ def import!(gh_project_name, target_group_path, project_name)
+ within_element(:project_import_row, source_project: gh_project_name) do
+ click_element(:target_namespace_selector_dropdown)
+ click_element(:target_group_dropdown_item, group_name: target_group_path)
+ fill_element(:project_path_field, project_name)
click_element(:import_button)
end
end
- def wait_for_success
- # TODO: set reload:false and remove skip_finished_loading_check_on_refresh when
- # https://gitlab.com/gitlab-org/gitlab/-/issues/292861 is fixed
- wait_until(
- max_duration: 90,
- sleep_interval: 5.0,
- reload: true,
- skip_finished_loading_check_on_refresh: true
- ) do
- # TODO: Refactor to explicitly wait for specific project import successful status
- # This check can create false positive if main importing message appears with delay and check exits early
- page.has_no_content?('Importing 1 repository', wait: 3)
+ # Check Go to project button present
+ #
+ # @param [String] gh_project_name
+ # @return [Boolean]
+ def has_go_to_project_button?(gh_project_name)
+ within_element(:project_import_row, source_project: gh_project_name) do
+ has_element?(:go_to_project_button)
end
end
- def already_imported(full_path)
- within_repo_path(full_path) do
- has_element?(:project_path_content) && has_element?(:go_to_project_button)
+ # Check if import page has a successfully imported project
+ #
+ # @param [String] source_project_name
+ # @param [Integer] wait
+ # @return [Boolean]
+ def has_imported_project?(gh_project_name, wait: QA::Support::WaitForRequests::DEFAULT_MAX_WAIT_TIME)
+ within_element(:project_import_row, source_project: gh_project_name, skip_finished_loading_check: true) do
+ # TODO: remove retrier with reload:true once https://gitlab.com/gitlab-org/gitlab/-/issues/292861 is fixed
+ wait_until(
+ max_duration: wait,
+ sleep_interval: 5,
+ reload: true,
+ skip_finished_loading_check_on_refresh: true
+ ) do
+ has_element?(:import_status_indicator, text: "Complete")
+ end
end
end
+
+ alias_method :wait_for_success, :has_imported_project?
end
end
end
diff --git a/qa/qa/page/project/new.rb b/qa/qa/page/project/new.rb
index 170cc14b27f..06e476f009a 100644
--- a/qa/qa/page/project/new.rb
+++ b/qa/qa/page/project/new.rb
@@ -83,8 +83,8 @@ module QA
click_button 'Repo by URL'
end
- def enable_initialize_with_readme
- check_element(:initialize_with_readme_checkbox)
+ def disable_initialize_with_readme
+ uncheck_element(:initialize_with_readme_checkbox)
end
end
end
diff --git a/qa/qa/page/project/registry/show.rb b/qa/qa/page/project/registry/show.rb
index dffdb9eebba..03c547fc8b5 100644
--- a/qa/qa/page/project/registry/show.rb
+++ b/qa/qa/page/project/registry/show.rb
@@ -31,7 +31,7 @@ module QA
def click_delete
click_element(:tag_delete_button)
- find_button('Confirm').click
+ find_button('Delete').click
end
end
end
diff --git a/qa/qa/page/project/secure/configuration_form.rb b/qa/qa/page/project/secure/configuration_form.rb
new file mode 100644
index 00000000000..73d1601b61e
--- /dev/null
+++ b/qa/qa/page/project/secure/configuration_form.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module QA
+ module Page
+ module Project
+ module Secure
+ class ConfigurationForm < QA::Page::Base
+ include QA::Page::Component::Select2
+ include QA::Page::Settings::Common
+
+ view 'app/assets/javascripts/security_configuration/components/feature_card.vue' do
+ element :sast_status, "`${feature.type}_status`" # rubocop:disable QA/ElementWithPattern
+ element :sast_enable_button, "`${feature.type}_enable_button`" # rubocop:disable QA/ElementWithPattern
+ end
+
+ def click_sast_enable_button
+ click_element(:sast_enable_button)
+ end
+
+ def has_sast_status?(status_text)
+ within_element(:sast_status) do
+ has_text?(status_text)
+ end
+ end
+ end
+ end
+ end
+ end
+end
+
+QA::Page::Project::Secure::ConfigurationForm.prepend_mod_with('Page::Project::Secure::ConfigurationForm', namespace: QA)
diff --git a/qa/qa/page/project/wiki/edit.rb b/qa/qa/page/project/wiki/edit.rb
index 70aa10cc43e..e782bbbb432 100644
--- a/qa/qa/page/project/wiki/edit.rb
+++ b/qa/qa/page/project/wiki/edit.rb
@@ -7,6 +7,7 @@ module QA
class Edit < Base
include Page::Component::WikiPageForm
include Page::Component::WikiSidebar
+ include Page::Component::ContentEditor
end
end
end
diff --git a/qa/qa/resource/base.rb b/qa/qa/resource/base.rb
index ca0087cf709..2848e3ba7d2 100644
--- a/qa/qa/resource/base.rb
+++ b/qa/qa/resource/base.rb
@@ -75,19 +75,18 @@ module QA
end
def log_fabrication(method, resource, parents, args)
- return yield unless Runtime::Env.debug?
-
start = Time.now
- prefix = "==#{'=' * parents.size}>"
- msg = [prefix]
- msg << "Built a #{name}"
- msg << "as a dependency of #{parents.last}" if parents.any?
- msg << "via #{method}"
yield.tap do
- msg << "in #{Time.now - start} seconds"
- puts msg.join(' ')
- puts if parents.empty?
+ Runtime::Logger.debug do
+ msg = ["==#{'=' * parents.size}>"]
+ msg << "Built a #{name}"
+ msg << "as a dependency of #{parents.last}" if parents.any?
+ msg << "via #{method}"
+ msg << "in #{Time.now - start} seconds"
+
+ msg.join(' ')
+ end
end
end
@@ -189,7 +188,7 @@ module QA
end
def log_having_both_api_result_and_block(name, api_value)
- QA::Runtime::Logger.info(<<~MSG.strip)
+ QA::Runtime::Logger.debug(<<~MSG.strip)
<#{self.class}> Attribute #{name.inspect} has both API response `#{api_value}` and a block. API response will be picked. Block will be ignored.
MSG
end
diff --git a/qa/qa/resource/bulk_import_group.rb b/qa/qa/resource/bulk_import_group.rb
new file mode 100644
index 00000000000..5380bb16f10
--- /dev/null
+++ b/qa/qa/resource/bulk_import_group.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+
+module QA
+ module Resource
+ class BulkImportGroup < Group
+ attributes :source_group_path,
+ :import_id
+
+ attribute :destination_group_path do
+ source_group_path
+ end
+
+ attribute :access_token do
+ api_client.personal_access_token
+ end
+
+ alias_method :path, :source_group_path
+
+ delegate :gitlab_address, to: 'QA::Runtime::Scenario'
+
+ def fabricate_via_browser_ui!
+ Page::Main::Menu.perform(&:go_to_create_group)
+
+ Page::Group::New.perform do |group|
+ group.switch_to_import_tab
+ group.connect_gitlab_instance(gitlab_address, api_client.personal_access_token)
+ end
+
+ Page::Group::BulkImport.perform do |import_page|
+ import_page.import_group(path, sandbox.path)
+ end
+
+ reload!
+ visit!
+ end
+
+ def fabricate_via_api!
+ resource_web_url(api_post)
+ end
+
+ def api_post_path
+ '/bulk_imports'
+ end
+
+ def api_post_body
+ {
+ configuration: {
+ url: gitlab_address,
+ access_token: access_token
+ },
+ entities: [
+ {
+ source_type: 'group_entity',
+ source_full_path: source_group_path,
+ destination_name: destination_group_path,
+ destination_namespace: sandbox.path
+ }
+ ]
+ }
+ end
+
+ def import_status
+ response = get(Runtime::API::Request.new(api_client, "/bulk_imports/#{import_id}").url)
+
+ unless response.code == HTTP_STATUS_OK
+ raise ResourceQueryError, "Could not get import status. Request returned (#{response.code}): `#{response}`."
+ end
+
+ parse_body(response)[:status]
+ end
+
+ private
+
+ def transform_api_resource(api_resource)
+ return api_resource if api_resource[:web_url]
+
+ # override transformation only for /bulk_imports endpoint which doesn't have web_url in response and
+ # ignore others so import_id is not overwritten incorrectly
+ api_resource[:web_url] = "#{gitlab_address}/#{full_path}"
+ api_resource[:import_id] = api_resource[:id]
+ api_resource
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/group_base.rb b/qa/qa/resource/group_base.rb
index 652c6cf7d1e..b937b704613 100644
--- a/qa/qa/resource/group_base.rb
+++ b/qa/qa/resource/group_base.rb
@@ -101,3 +101,5 @@ module QA
end
end
end
+
+QA::Resource::GroupBase.prepend_mod_with('Resource::GroupBase', namespace: QA)
diff --git a/qa/qa/resource/issue.rb b/qa/qa/resource/issue.rb
index ffffa0eecda..c45ab7593b6 100644
--- a/qa/qa/resource/issue.rb
+++ b/qa/qa/resource/issue.rb
@@ -14,11 +14,11 @@ module QA
end
end
- attribute :id
- attribute :iid
- attribute :assignee_ids
- attribute :labels
- attribute :title
+ attributes :id,
+ :iid,
+ :assignee_ids,
+ :labels,
+ :title
def initialize
@assignee_ids = []
@@ -41,13 +41,21 @@ module QA
end
def api_get_path
- "/projects/#{project.id}/issues/#{id}"
+ "/projects/#{project.id}/issues/#{iid}"
end
def api_post_path
"/projects/#{project.id}/issues"
end
+ def api_put_path
+ "/projects/#{project.id}/issues/#{iid}"
+ end
+
+ def api_comments_path
+ "#{api_get_path}/notes"
+ end
+
def api_post_body
{
assignee_ids: assignee_ids,
@@ -59,20 +67,31 @@ module QA
end
end
- def api_put_path
- "/projects/#{project.id}/issues/#{iid}"
- end
-
def set_issue_assignees(assignee_ids:)
put_body = { assignee_ids: assignee_ids }
response = put Runtime::API::Request.new(api_client, api_put_path).url, put_body
unless response.code == HTTP_STATUS_OK
- raise ResourceUpdateFailedError, "Could not update issue assignees to #{assignee_ids}. Request returned (#{response.code}): `#{response}`."
+ raise(
+ ResourceUpdateFailedError,
+ "Could not update issue assignees to #{assignee_ids}. Request returned (#{response.code}): `#{response}`."
+ )
end
QA::Runtime::Logger.debug("Successfully updated issue assignees to #{assignee_ids}")
end
+
+ # Get issue comments
+ #
+ # @return [Array]
+ def comments(auto_paginate: false, attempts: 0)
+ return parse_body(api_get_from(api_comments_path)) unless auto_paginate
+
+ auto_paginated_response(
+ Runtime::API::Request.new(api_client, api_comments_path, per_page: '100').url,
+ attempts: attempts
+ )
+ end
end
end
end
diff --git a/qa/qa/resource/merge_request.rb b/qa/qa/resource/merge_request.rb
index 8d9de0ea718..8c313f5d518 100644
--- a/qa/qa/resource/merge_request.rb
+++ b/qa/qa/resource/merge_request.rb
@@ -152,7 +152,8 @@ module QA
@project = Resource::ImportProject.fabricate_via_browser_ui!
# Setting the name here, since otherwise some tests will look for an existing file in
# the proejct without ever knowing what is in it.
- @file_name = "github_controller_spec.rb"
+ @file_name = "added_file-00000000.txt"
+ @source_branch = "large_merge_request"
visit("#{project.web_url}/-/merge_requests/1")
current_url
end
@@ -160,9 +161,13 @@ module QA
# Get MR comments
#
# @return [Array]
- def comments
- response = get(Runtime::API::Request.new(api_client, api_comments_path).url)
- parse_body(response)
+ def comments(auto_paginate: false, attempts: 0)
+ return parse_body(api_get_from(api_comments_path)) unless auto_paginate
+
+ auto_paginated_response(
+ Runtime::API::Request.new(api_client, api_comments_path, per_page: '100').url,
+ attempts: attempts
+ )
end
private
diff --git a/qa/qa/resource/project.rb b/qa/qa/resource/project.rb
index d111b070863..53b8a9b0246 100644
--- a/qa/qa/resource/project.rb
+++ b/qa/qa/resource/project.rb
@@ -13,13 +13,14 @@ module QA
:initialize_with_readme,
:auto_devops_enabled,
:github_personal_access_token,
- :github_repository_path
+ :github_repository_path,
+ :gitlab_repository_path
attributes :id,
:name,
:add_name_uuid,
:description,
- :standalone,
+ :personal_namespace,
:runners_token,
:visibility,
:template_name,
@@ -32,7 +33,7 @@ module QA
end
attribute :path_with_namespace do
- "#{sandbox_path}#{group.path}/#{name}" if group
+ "#{group.full_path}/#{name}"
end
alias_method :full_path, :path_with_namespace
@@ -51,7 +52,7 @@ module QA
def initialize
@add_name_uuid = true
- @standalone = false
+ @personal_namespace = false
@description = 'My awesome project'
@initialize_with_readme = false
@auto_devops_enabled = false
@@ -69,7 +70,9 @@ module QA
def fabricate!
return if @import
- unless @standalone
+ if @personal_namespace
+ Page::Dashboard::Projects.perform(&:click_new_project_button)
+ else
group.visit!
Page::Group::Show.perform(&:go_to_new_project)
end
@@ -84,13 +87,15 @@ module QA
Page::Project::New.perform(&:click_blank_project_link)
Page::Project::New.perform do |new_page|
- new_page.choose_test_namespace
+ new_page.choose_test_namespace unless @personal_namespace
new_page.choose_name(@name)
new_page.add_description(@description)
new_page.set_visibility(@visibility)
- new_page.enable_initialize_with_readme if @initialize_with_readme
+ new_page.disable_initialize_with_readme unless @initialize_with_readme
new_page.create_new_project
end
+
+ @id = Page::Project::Show.perform(&:project_id)
end
def fabricate_via_api!
@@ -218,7 +223,7 @@ module QA
auto_devops_enabled: @auto_devops_enabled
}
- unless @standalone
+ unless @personal_namespace
post_body[:namespace_id] = group.id
post_body[:path] = name
end
@@ -263,19 +268,24 @@ module QA
result = parse_body(response)
- Runtime::Logger.error("Import failed: #{result[:import_error]}") if result[:import_status] == "failed"
+ if result[:import_status] == "failed"
+ Runtime::Logger.error("Import failed: #{result[:import_error]}")
+ Runtime::Logger.error("Failed relations: #{result[:failed_relations]}")
+ end
result[:import_status]
end
- def commits
- response = get(request_url(api_commits_path))
- parse_body(response)
+ def commits(auto_paginate: false, attempts: 0)
+ return parse_body(api_get_from(api_commits_path)) unless auto_paginate
+
+ auto_paginated_response(request_url(api_commits_path, per_page: '100'), attempts: attempts)
end
- def merge_requests
- response = get(request_url(api_merge_requests_path))
- parse_body(response)
+ def merge_requests(auto_paginate: false, attempts: 0)
+ return parse_body(api_get_from(api_merge_requests_path)) unless auto_paginate
+
+ auto_paginated_response(request_url(api_merge_requests_path, per_page: '100'), attempts: attempts)
end
def merge_request_with_title(title)
@@ -299,9 +309,10 @@ module QA
parse_body(response)
end
- def repository_branches
- response = get(request_url(api_repository_branches_path))
- parse_body(response)
+ def repository_branches(auto_paginate: false, attempts: 0)
+ return parse_body(api_get_from(api_repository_branches_path)) unless auto_paginate
+
+ auto_paginated_response(request_url(api_repository_branches_path, per_page: '100'), attempts: attempts)
end
def repository_tags
@@ -324,19 +335,22 @@ module QA
parse_body(response)
end
- def issues
- response = get(request_url(api_issues_path))
- parse_body(response)
+ def issues(auto_paginate: false, attempts: 0)
+ return parse_body(api_get_from(api_issues_path)) unless auto_paginate
+
+ auto_paginated_response(request_url(api_issues_path, per_page: '100'), attempts: attempts)
end
- def labels
- response = get(request_url(api_labels_path))
- parse_body(response)
+ def labels(auto_paginate: false, attempts: 0)
+ return parse_body(api_get_from(api_labels_path)) unless auto_paginate
+
+ auto_paginated_response(request_url(api_labels_path, per_page: '100'), attempts: attempts)
end
- def milestones
- response = get(request_url(api_milestones_path))
- parse_body(response)
+ def milestones(auto_paginate: false, attempts: 0)
+ return parse_body(api_get_from(api_milestones_path)) unless auto_paginate
+
+ auto_paginated_response(request_url(api_milestones_path, per_page: '100'), attempts: attempts)
end
def wikis
diff --git a/qa/qa/resource/project_imported_from_github.rb b/qa/qa/resource/project_imported_from_github.rb
index 214e8f517bb..8aa19555d50 100644
--- a/qa/qa/resource/project_imported_from_github.rb
+++ b/qa/qa/resource/project_imported_from_github.rb
@@ -18,9 +18,12 @@ module QA
Page::Project::Import::Github.perform do |import_page|
import_page.add_personal_access_token(github_personal_access_token)
- import_page.import!(github_repository_path, name)
- import_page.go_to_project(name)
+ import_page.import!(github_repository_path, group.full_path, name)
+ import_page.wait_for_success(github_repository_path)
end
+
+ reload!
+ visit!
end
def go_to_import_page
diff --git a/qa/qa/runtime/allure_report.rb b/qa/qa/runtime/allure_report.rb
index bcfdb09e09f..bf49141566a 100644
--- a/qa/qa/runtime/allure_report.rb
+++ b/qa/qa/runtime/allure_report.rb
@@ -67,25 +67,8 @@ module QA
# @return [void]
def configure_rspec
RSpec.configure do |config|
- config.formatter = AllureRspecFormatter
-
- config.after do |example|
- next if example.attempts && example.attempts > 0
-
- testcase = example.metadata[:testcase]
- example.tms('Testcase', testcase) if testcase
-
- quarantine_issue = example.metadata.dig(:quarantine, :issue)
- example.issue('Quarantine issue', quarantine_issue) if quarantine_issue
-
- spec_file = example.file_path.split('/').last
- example.issue(
- 'Failure issues',
- "https://gitlab.com/gitlab-org/gitlab/-/issues?scope=all&state=opened&search=#{spec_file}"
- )
-
- example.add_link(name: "Job(#{Env.ci_job_name})", url: Env.ci_job_url) if Env.running_in_ci?
- end
+ config.add_formatter(AllureRspecFormatter)
+ config.add_formatter(QA::Support::AllureMetadataFormatter)
end
end
diff --git a/qa/qa/runtime/api/request.rb b/qa/qa/runtime/api/request.rb
index 28bae541cb8..c1df5e84f6c 100644
--- a/qa/qa/runtime/api/request.rb
+++ b/qa/qa/runtime/api/request.rb
@@ -6,6 +6,10 @@ module QA
class Request
API_VERSION = 'v4'
+ def self.masked_url(url)
+ url.sub(/private_token=.*/, "private_token=[****]")
+ end
+
def initialize(api_client, path, **query_string)
query_string[:private_token] ||= api_client.personal_access_token unless query_string[:oauth_access_token]
request_path = request_path(path, **query_string)
@@ -13,7 +17,7 @@ module QA
end
def mask_url
- @session_address.address.sub(/private_token=.*/, "private_token=[****]")
+ QA::Runtime::API::Request.masked_url(url)
end
def url
diff --git a/qa/qa/runtime/application_settings.rb b/qa/qa/runtime/application_settings.rb
index 428ed20c83f..0b2aef47576 100644
--- a/qa/qa/runtime/application_settings.rb
+++ b/qa/qa/runtime/application_settings.rb
@@ -26,7 +26,7 @@ module QA
end
def restore_application_settings(*application_settings_keys)
- set_application_settings(@original_application_settings.slice(*application_settings_keys))
+ set_application_settings(**@original_application_settings.slice(*application_settings_keys))
end
private
diff --git a/qa/qa/runtime/browser.rb b/qa/qa/runtime/browser.rb
index e13061e2648..9097690de57 100644
--- a/qa/qa/runtime/browser.rb
+++ b/qa/qa/runtime/browser.rb
@@ -143,11 +143,12 @@ module QA
if QA::Runtime::Env.remote_grid
selenium_options[:url] = QA::Runtime::Env.remote_grid
capabilities[:browserVersion] = 'latest'
+ capabilities['sauce:options'] = { tunnelIdentifier: QA::Runtime::Env.remote_tunnel_id }
end
Capybara::Selenium::Driver.new(
app,
- selenium_options
+ **selenium_options
)
end
diff --git a/qa/qa/runtime/env.rb b/qa/qa/runtime/env.rb
index 5cac811d95b..a076d8db9e0 100644
--- a/qa/qa/runtime/env.rb
+++ b/qa/qa/runtime/env.rb
@@ -142,6 +142,10 @@ module QA
ENV['QA_REMOTE_GRID_PROTOCOL'] || 'http'
end
+ def remote_tunnel_id
+ ENV['QA_REMOTE_TUNNEL_ID'] || 'gitlab-sl_tunnel_id'
+ end
+
def browser
ENV['QA_BROWSER'].nil? ? :chrome : ENV['QA_BROWSER'].to_sym
end
diff --git a/qa/qa/runtime/feature.rb b/qa/qa/runtime/feature.rb
index dd7f9cf898c..7011f46542b 100644
--- a/qa/qa/runtime/feature.rb
+++ b/qa/qa/runtime/feature.rb
@@ -32,7 +32,7 @@ module QA
def enabled?(key, **scopes)
feature = JSON.parse(get_features).find { |flag| flag['name'] == key.to_s }
- feature && (feature['state'] == 'on' || feature['state'] == 'conditional' && scopes.present? && enabled_scope?(feature['gates'], scopes))
+ feature && (feature['state'] == 'on' || feature['state'] == 'conditional' && scopes.present? && enabled_scope?(feature['gates'], **scopes))
end
private
@@ -43,7 +43,7 @@ module QA
raise AuthorizationError, "Administrator access is required to enable/disable feature flags. #{e.message}"
end
- def enabled_scope?(gates, scopes)
+ def enabled_scope?(gates, **scopes)
scopes.each do |key, value|
case key
when :project, :group, :user
@@ -71,16 +71,16 @@ module QA
# scopes: Any scope (user, project, group) to restrict the change to
def set_and_verify(key, enable:, **scopes)
msg = "#{enable ? 'En' : 'Dis'}abling feature: #{key}"
- msg += " for scope \"#{scopes_to_s(scopes)}\"" if scopes.present?
+ msg += " for scope \"#{scopes_to_s(**scopes)}\"" if scopes.present?
QA::Runtime::Logger.info(msg)
Support::Retrier.retry_on_exception(sleep_interval: 2) do
- set_feature(key, enable, scopes)
+ set_feature(key, enable, **scopes)
is_enabled = nil
QA::Support::Waiter.wait_until(sleep_interval: 1) do
- is_enabled = enabled?(key, scopes)
+ is_enabled = enabled?(key, **scopes)
is_enabled == enable || !enable && scopes.present?
end
diff --git a/qa/qa/specs/features/api/1_manage/bulk_import_group_spec.rb b/qa/qa/specs/features/api/1_manage/bulk_import_group_spec.rb
new file mode 100644
index 00000000000..6bbb859b3ee
--- /dev/null
+++ b/qa/qa/specs/features/api/1_manage/bulk_import_group_spec.rb
@@ -0,0 +1,102 @@
+# frozen_string_literal: true
+
+module QA
+ RSpec.describe 'Manage', :requires_admin do
+ describe 'Bulk group import' do
+ let!(:staging?) { Runtime::Scenario.gitlab_address.include?('staging.gitlab.com') }
+
+ let(:admin_api_client) { Runtime::API::Client.as_admin }
+ let(:user) do
+ Resource::User.fabricate_via_api! do |usr|
+ usr.api_client = admin_api_client
+ usr.hard_delete_on_api_removal = true
+ end
+ end
+
+ let(:api_client) { Runtime::API::Client.new(user: user) }
+ let(:personal_access_token) { api_client.personal_access_token }
+
+ let(:sandbox) do
+ Resource::Sandbox.fabricate_via_api! do |group|
+ group.api_client = admin_api_client
+ end
+ end
+
+ let(:source_group) do
+ Resource::Sandbox.fabricate_via_api! do |group|
+ group.api_client = api_client
+ group.path = "source-group-for-import-#{SecureRandom.hex(4)}"
+ end
+ end
+
+ let(:subgroup) do
+ Resource::Group.fabricate_via_api! do |group|
+ group.api_client = api_client
+ group.sandbox = source_group
+ group.path = "subgroup-for-import-#{SecureRandom.hex(4)}"
+ end
+ end
+
+ let(:imported_subgroup) do
+ Resource::Group.init do |group|
+ group.api_client = api_client
+ group.sandbox = imported_group
+ group.path = subgroup.path
+ end
+ end
+
+ let(:imported_group) do
+ Resource::BulkImportGroup.fabricate_via_api! do |group|
+ group.api_client = api_client
+ group.sandbox = sandbox
+ group.source_group_path = source_group.path
+ end
+ end
+
+ before do
+ Runtime::Feature.enable(:bulk_import) unless staging?
+ Runtime::Feature.enable(:top_level_group_creation_enabled) if staging?
+
+ sandbox.add_member(user, Resource::Members::AccessLevel::MAINTAINER)
+
+ Resource::GroupLabel.fabricate_via_api! do |label|
+ label.api_client = api_client
+ label.group = source_group
+ label.title = "source-group-#{SecureRandom.hex(4)}"
+ end
+ Resource::GroupLabel.fabricate_via_api! do |label|
+ label.api_client = api_client
+ label.group = subgroup
+ label.title = "subgroup-#{SecureRandom.hex(4)}"
+ end
+ end
+
+ # Non blocking issues:
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/331252
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/333678 <- can cause 500 when creating user and group back to back
+ it(
+ 'imports group with subgroups and labels',
+ testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1871'
+ ) do
+ expect { imported_group.import_status }.to(
+ eventually_eq('finished').within(max_duration: 300, sleep_interval: 2)
+ )
+
+ aggregate_failures do
+ expect(imported_group.reload!).to eq(source_group)
+ expect(imported_group.labels).to include(*source_group.labels)
+
+ expect(imported_subgroup.reload!).to eq(subgroup)
+ expect(imported_subgroup.labels).to include(*subgroup.labels)
+ end
+ end
+
+ after do
+ user.remove_via_api!
+ ensure
+ Runtime::Feature.disable(:bulk_import) unless staging?
+ Runtime::Feature.disable(:top_level_group_creation_enabled) if staging?
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/api/1_manage/import_github_repo_spec.rb b/qa/qa/specs/features/api/1_manage/import_github_repo_spec.rb
index 1b873d35d75..72a0a761294 100644
--- a/qa/qa/specs/features/api/1_manage/import_github_repo_spec.rb
+++ b/qa/qa/specs/features/api/1_manage/import_github_repo_spec.rb
@@ -33,7 +33,7 @@ module QA
it 'imports Github repo via api', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1858' do
imported_project # import the project
- expect { imported_project.reload!.import_status }.to eventually_eq('finished').within(duration: 90)
+ expect { imported_project.reload!.import_status }.to eventually_eq('finished').within(max_duration: 90)
aggregate_failures do
verify_repository_import
diff --git a/qa/qa/specs/features/api/1_manage/import_large_github_repo_spec.rb b/qa/qa/specs/features/api/1_manage/import_large_github_repo_spec.rb
new file mode 100644
index 00000000000..385908f2176
--- /dev/null
+++ b/qa/qa/specs/features/api/1_manage/import_large_github_repo_spec.rb
@@ -0,0 +1,362 @@
+# frozen_string_literal: true
+
+require 'octokit'
+
+# rubocop:disable Rails/Pluck
+module QA
+ # Only executes in custom job/pipeline
+ RSpec.describe 'Manage', :github, :requires_admin, only: { job: 'large-github-import' } do
+ describe 'Project import' do
+ let(:logger) { Runtime::Logger.logger }
+ let(:differ) { RSpec::Support::Differ.new(color: true) }
+
+ let(:api_client) { Runtime::API::Client.as_admin }
+ let(:group) do
+ Resource::Group.fabricate_via_api! do |resource|
+ resource.api_client = api_client
+ end
+ end
+
+ let(:user) do
+ Resource::User.fabricate_via_api! do |resource|
+ resource.api_client = api_client
+ end
+ end
+
+ let(:github_repo) { ENV['QA_LARGE_GH_IMPORT_REPO'] || 'rspec/rspec-core' }
+ let(:import_max_duration) { ENV['QA_LARGE_GH_IMPORT_DURATION'] ? ENV['QA_LARGE_GH_IMPORT_DURATION'].to_i : 7200 }
+ let(:github_client) do
+ Octokit.middleware = Faraday::RackBuilder.new do |builder|
+ builder.response(:logger, logger, headers: false, bodies: false)
+ end
+
+ Octokit::Client.new(
+ access_token: ENV['QA_LARGE_GH_IMPORT_GH_TOKEN'] || Runtime::Env.github_access_token,
+ auto_paginate: true
+ )
+ end
+
+ let(:gh_branches) { github_client.branches(github_repo).map(&:name) }
+ let(:gh_commits) { github_client.commits(github_repo).map(&:sha) }
+ let(:gh_repo) { github_client.repository(github_repo) }
+
+ let(:gh_labels) do
+ github_client.labels(github_repo).map { |label| { name: label.name, color: "##{label.color}" } }
+ end
+
+ let(:gh_milestones) do
+ github_client
+ .list_milestones(github_repo, state: 'all')
+ .map { |ms| { title: ms.title, description: ms.description } }
+ end
+
+ let(:gh_all_issues) do
+ github_client.list_issues(github_repo, state: 'all')
+ end
+
+ let(:gh_prs) do
+ gh_all_issues.select(&:pull_request).each_with_object({}) do |pr, hash|
+ hash[pr.title] = {
+ body: pr.body || '',
+ comments: [*gh_pr_comments[pr.html_url], *gh_issue_comments[pr.html_url]].compact.sort
+ }
+ end
+ end
+
+ let(:gh_issues) do
+ gh_all_issues.reject(&:pull_request).each_with_object({}) do |issue, hash|
+ hash[issue.title] = {
+ body: issue.body || '',
+ comments: gh_issue_comments[issue.html_url]
+ }
+ end
+ end
+
+ let(:gh_issue_comments) do
+ github_client.issues_comments(github_repo).each_with_object(Hash.new { |h, k| h[k] = [] }) do |c, hash|
+ hash[c.html_url.gsub(/\#\S+/, "")] << c.body # use base html url as key
+ end
+ end
+
+ let(:gh_pr_comments) do
+ github_client.pull_requests_comments(github_repo).each_with_object(Hash.new { |h, k| h[k] = [] }) do |c, hash|
+ hash[c.html_url.gsub(/\#\S+/, "")] << c.body # use base html url as key
+ end
+ end
+
+ let(:imported_project) do
+ Resource::ProjectImportedFromGithub.fabricate_via_api! do |project|
+ project.add_name_uuid = false
+ project.name = 'imported-project'
+ project.group = group
+ project.github_personal_access_token = Runtime::Env.github_access_token
+ project.github_repository_path = github_repo
+ project.api_client = api_client
+ end
+ end
+
+ before do
+ group.add_member(user, Resource::Members::AccessLevel::MAINTAINER)
+ end
+
+ after do |example|
+ user.remove_via_api!
+ next unless defined?(@import_time)
+
+ # save data for comparison after run finished
+ save_json(
+ "data",
+ {
+ import_time: @import_time,
+ github: {
+ project_name: github_repo,
+ branches: gh_branches,
+ commits: gh_commits,
+ labels: gh_labels,
+ milestones: gh_milestones,
+ prs: gh_prs,
+ issues: gh_issues
+ },
+ gitlab: {
+ project_name: imported_project.path_with_namespace,
+ branches: gl_branches,
+ commits: gl_commits,
+ labels: gl_labels,
+ milestones: gl_milestones,
+ mrs: mrs,
+ issues: gl_issues
+ }
+ }.to_json
+ )
+ end
+
+ it 'imports large Github repo via api' do
+ start = Time.now
+
+ imported_project # import the project
+ fetch_github_objects # fetch all objects right after import has started
+
+ import_status = lambda do
+ imported_project.reload!.import_status.tap do |status|
+ raise "Import of '#{imported_project.name}' failed!" if status == 'failed'
+ end
+ end
+ expect(import_status).to eventually_eq('finished').within(max_duration: import_max_duration, sleep_interval: 30)
+ @import_time = Time.now - start
+
+ aggregate_failures do
+ verify_repository_import
+ verify_labels_import
+ verify_milestones_import
+ verify_merge_requests_import
+ verify_issues_import
+ end
+ end
+
+ # Persist all objects from repository being imported
+ #
+ # @return [void]
+ def fetch_github_objects
+ logger.debug("== Fetching objects for github repo: '#{github_repo}' ==")
+
+ gh_repo
+ gh_branches
+ gh_commits
+ gh_prs
+ gh_issues
+ gh_labels
+ gh_milestones
+ end
+
+ # Verify repository imported correctly
+ #
+ # @return [void]
+ def verify_repository_import
+ logger.debug("== Verifying repository import ==")
+ expect(imported_project.description).to eq(gh_repo.description)
+ # check via include, importer creates more branches
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/332711
+ expect(gl_branches).to include(*gh_branches)
+ expect(gl_commits).to match_array(gh_commits)
+ end
+
+ # Verify imported merge requests and mr issues
+ #
+ # @return [void]
+ def verify_merge_requests_import
+ logger.debug("== Verifying merge request import ==")
+ verify_mrs_or_issues('mr')
+ end
+
+ # Verify imported issues and issue comments
+ #
+ # @return [void]
+ def verify_issues_import
+ logger.debug("== Verifying issue import ==")
+ verify_mrs_or_issues('issue')
+ end
+
+ # Verify imported labels
+ #
+ # @return [void]
+ def verify_labels_import
+ logger.debug("== Verifying label import ==")
+ # check via include, additional labels can be inherited from parent group
+ expect(gl_labels).to include(*gh_labels)
+ end
+
+ # Verify milestones import
+ #
+ # @return [void]
+ def verify_milestones_import
+ logger.debug("== Verifying milestones import ==")
+ expect(gl_milestones).to match_array(gh_milestones)
+ end
+
+ private
+
+ # Verify imported mrs or issues
+ #
+ # @param [String] type verification object, 'mrs' or 'issues'
+ # @return [void]
+ def verify_mrs_or_issues(type)
+ msg = ->(title) { "expected #{type} with title '#{title}' to have" }
+ expected = type == 'mr' ? mrs : gl_issues
+ actual = type == 'mr' ? gh_prs : gh_issues
+
+ # Compare length to have easy to read overview how many objects are missing
+ expect(expected.length).to(
+ eq(actual.length),
+ "Expected to contain same amount of #{type}s. Expected: #{expected.length}, actual: #{actual.length}"
+ )
+ logger.debug("= Comparing #{type}s =")
+ actual.each do |title, actual_item|
+ print "." # indicate that it is still going but don't spam the output with newlines
+
+ expected_item = expected[title]
+
+ expect(expected_item).to be_truthy, "#{msg.call(title)} been imported"
+ next unless expected_item
+
+ expect(expected_item[:body]).to(
+ include(actual_item[:body]),
+ "#{msg.call(title)} same description. diff:\n#{differ.diff(expected_item[:body], actual_item[:body])}"
+ )
+ expect(expected_item[:comments].length).to(
+ eq(actual_item[:comments].length),
+ "#{msg.call(title)} same amount of comments"
+ )
+ expect(expected_item[:comments]).to match_array(actual_item[:comments])
+ end
+ puts # print newline after last print to make output pretty
+ end
+
+ # Imported project branches
+ #
+ # @return [Array]
+ def gl_branches
+ @gl_branches ||= begin
+ logger.debug("= Fetching branches =")
+ imported_project.repository_branches(auto_paginate: true).map { |b| b[:name] }
+ end
+ end
+
+ # Imported project commits
+ #
+ # @return [Array]
+ def gl_commits
+ @gl_commits ||= begin
+ logger.debug("= Fetching commits =")
+ imported_project.commits(auto_paginate: true, attempts: 2).map { |c| c[:id] }
+ end
+ end
+
+ # Imported project labels
+ #
+ # @return [Array]
+ def gl_labels
+ @gl_labels ||= begin
+ logger.debug("= Fetching labels =")
+ imported_project.labels(auto_paginate: true).map { |label| label.slice(:name, :color) }
+ end
+ end
+
+ # Imported project milestones
+ #
+ # @return [<Type>] <description>
+ def gl_milestones
+ @gl_milestones ||= begin
+ logger.debug("= Fetching milestones =")
+ imported_project.milestones(auto_paginate: true).map { |ms| ms.slice(:title, :description) }
+ end
+ end
+
+ # Imported project merge requests
+ #
+ # @return [Hash]
+ def mrs
+ @mrs ||= begin
+ logger.debug("= Fetching merge requests =")
+ imported_mrs = imported_project.merge_requests(auto_paginate: true, attempts: 2)
+ logger.debug("= Transforming merge request objects for comparison =")
+ imported_mrs.each_with_object({}) do |mr, hash|
+ resource = Resource::MergeRequest.init do |resource|
+ resource.project = imported_project
+ resource.iid = mr[:iid]
+ resource.api_client = api_client
+ end
+
+ hash[mr[:title]] = {
+ body: mr[:description],
+ comments: resource.comments(auto_paginate: true, attempts: 2)
+ # remove system notes
+ .reject { |c| c[:system] || c[:body].match?(/^(\*\*Review:\*\*)|(\*Merged by:).*/) }
+ .map { |c| sanitize(c[:body]) }
+ }
+ end
+ end
+ end
+
+ # Imported project issues
+ #
+ # @return [Hash]
+ def gl_issues
+ @gl_issues ||= begin
+ logger.debug("= Fetching issues =")
+ imported_issues = imported_project.issues(auto_paginate: true, attempts: 2)
+ logger.debug("= Transforming issue objects for comparison =")
+ imported_issues.each_with_object({}) do |issue, hash|
+ resource = Resource::Issue.init do |issue_resource|
+ issue_resource.project = imported_project
+ issue_resource.iid = issue[:iid]
+ issue_resource.api_client = api_client
+ end
+
+ hash[issue[:title]] = {
+ body: issue[:description],
+ comments: resource.comments(auto_paginate: true, attempts: 2).map { |c| sanitize(c[:body]) }
+ }
+ end
+ end
+ end
+
+ # Remove added prefixes by importer
+ #
+ # @param [String] body
+ # @return [String]
+ def sanitize(body)
+ body.gsub(/\*Created by: \S+\*\n\n/, "")
+ end
+
+ # Save json as file
+ #
+ # @param [String] name
+ # @param [String] json
+ # @return [void]
+ def save_json(name, json)
+ File.open("tmp/#{name}.json", "w") { |file| file.write(json) }
+ end
+ end
+ end
+end
+# rubocop:enable Rails/Pluck
diff --git a/qa/qa/specs/features/api/3_create/merge_request/push_options_target_branch_spec.rb b/qa/qa/specs/features/api/3_create/merge_request/push_options_target_branch_spec.rb
index 799efc243d4..9ac27a2ca06 100644
--- a/qa/qa/specs/features/api/3_create/merge_request/push_options_target_branch_spec.rb
+++ b/qa/qa/specs/features/api/3_create/merge_request/push_options_target_branch_spec.rb
@@ -25,18 +25,6 @@ module QA
push.file_content = "Target branch test target branch #{SecureRandom.hex(8)}"
end
- # Confirm the target branch can be checked out to avoid a race condition
- # where the subsequent push option attempts to create an MR before the target branch is ready.
- Support::Retrier.retry_on_exception(sleep_interval: 5) do
- Git::Repository.perform do |repository|
- repository.uri = project.repository_http_location.uri
- repository.use_default_credentials
- repository.clone
- repository.configure_identity('GitLab QA', 'root@gitlab.com')
- repository.checkout(target_branch)
- end
- end
-
Resource::Repository::ProjectPush.fabricate! do |push|
push.project = project
push.branch_name = "push-options-test-#{SecureRandom.hex(8)}"
diff --git a/qa/qa/specs/features/api/3_create/repository/project_archive_compare_spec.rb b/qa/qa/specs/features/api/3_create/repository/project_archive_compare_spec.rb
index 4bd99b4820e..c65d981d99a 100644
--- a/qa/qa/specs/features/api/3_create/repository/project_archive_compare_spec.rb
+++ b/qa/qa/specs/features/api/3_create/repository/project_archive_compare_spec.rb
@@ -47,7 +47,7 @@ module QA
def create_project(user, api_client, project_name)
project = Resource::Project.fabricate_via_api! do |project|
- project.standalone = true
+ project.personal_namespace = true
project.add_name_uuid = false
project.name = project_name
project.path_with_namespace = "#{user.username}/#{project_name}"
diff --git a/qa/qa/specs/features/api/5_package/container_registry_spec.rb b/qa/qa/specs/features/api/5_package/container_registry_spec.rb
index 5003d49fe6c..f79a3ebbe03 100644
--- a/qa/qa/specs/features/api/5_package/container_registry_spec.rb
+++ b/qa/qa/specs/features/api/5_package/container_registry_spec.rb
@@ -3,7 +3,7 @@
require 'airborne'
module QA
- RSpec.describe 'Package', only: { subdomain: :staging } do
+ RSpec.describe 'Package', only: { subdomain: %i[staging pre] } do
include Support::Api
describe 'Container Registry' do
@@ -13,6 +13,7 @@ module QA
Resource::Project.fabricate_via_api! do |project|
project.name = 'project-with-registry-api'
project.template_name = 'express'
+ project.api_client = api_client
end
end
@@ -37,6 +38,12 @@ module QA
- docker:19.03.12-dind
variables:
IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
+ DOCKER_HOST: tcp://docker:2376
+ DOCKER_TLS_CERTDIR: "/certs"
+ DOCKER_TLS_VERIFY: 1
+ DOCKER_CERT_PATH: "$DOCKER_TLS_CERTDIR/client"
+ before_script:
+ - until docker info; do sleep 1; done
script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker build -t $IMAGE_TAG .
@@ -50,6 +57,7 @@ module QA
MEDIA_TYPE: 'application/vnd.docker.distribution.manifest.v2+json'
before_script:
- token=$(curl -u "$CI_REGISTRY_USER:$CI_REGISTRY_PASSWORD" "https://$CI_SERVER_HOST/jwt/auth?service=container_registry&scope=repository:$CI_PROJECT_PATH:pull,push,delete" | jq -r '.token')
+ - echo $token
script:
- 'digest=$(curl -L -H "Authorization: Bearer $token" -H "Accept: $MEDIA_TYPE" "https://$CI_REGISTRY/v2/$CI_PROJECT_PATH/manifests/master" | jq -r ".layers[0].digest")'
- 'curl -L -X DELETE -H "Authorization: Bearer $token" -H "Accept: $MEDIA_TYPE" "https://$CI_REGISTRY/v2/$CI_PROJECT_PATH/blobs/$digest"'
@@ -57,7 +65,6 @@ module QA
- 'digest=$(curl -L -H "Authorization: Bearer $token" -H "Accept: $MEDIA_TYPE" "https://$CI_REGISTRY/v2/$CI_PROJECT_PATH/manifests/master" | jq -r ".config.digest")'
- 'curl -L -X DELETE -H "Authorization: Bearer $token" -H "Accept: $MEDIA_TYPE" "https://$CI_REGISTRY/v2/$CI_PROJECT_PATH/manifests/$digest"'
- 'curl -L --head -H "Authorization: Bearer $token" -H "Accept: $MEDIA_TYPE" "https://$CI_REGISTRY/v2/$CI_PROJECT_PATH/manifests/$digest"'
-
YAML
end
@@ -67,8 +74,9 @@ module QA
it 'pushes, pulls image to the registry and deletes image blob, manifest and tag', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1738' do
Resource::Repository::Commit.fabricate_via_api! do |commit|
- commit.project = project
+ commit.api_client = api_client
commit.commit_message = 'Add .gitlab-ci.yml'
+ commit.project = project
commit.add_files([{
file_path: '.gitlab-ci.yml',
content: gitlab_ci_yaml
@@ -77,7 +85,7 @@ module QA
Support::Waiter.wait_until(max_duration: 10) { pipeline_is_triggered? }
- Support::Retrier.retry_until(max_duration: 260, sleep_interval: 5) do
+ Support::Retrier.retry_until(max_duration: 300, sleep_interval: 5) do
latest_pipeline_succeed?
end
diff --git a/qa/qa/specs/features/browser_ui/1_manage/group/bulk_import_group_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/group/bulk_import_group_spec.rb
index 6c2ff005f49..fe17b5c34e1 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/group/bulk_import_group_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/group/bulk_import_group_spec.rb
@@ -29,27 +29,11 @@ module QA
end
end
- let(:subgroup) do
- Resource::Group.fabricate_via_api! do |group|
- group.api_client = api_client
- group.sandbox = source_group
- group.path = "subgroup-for-import-#{SecureRandom.hex(4)}"
- end
- end
-
let(:imported_group) do
- Resource::Group.init do |group|
+ Resource::BulkImportGroup.init do |group|
group.api_client = api_client
group.sandbox = sandbox
- group.path = source_group.path
- end
- end
-
- let(:imported_subgroup) do
- Resource::Group.init do |group|
- group.api_client = api_client
- group.sandbox = imported_group
- group.path = subgroup.path
+ group.source_group_path = source_group.path
end
end
@@ -61,7 +45,6 @@ module QA
# create groups explicitly before connecting gitlab instance
source_group
- subgroup
Flow::Login.sign_in(as: user)
Page::Main::Menu.perform(&:go_to_create_group)
@@ -74,33 +57,15 @@ module QA
# Non blocking issues:
# https://gitlab.com/gitlab-org/gitlab/-/issues/331252
# https://gitlab.com/gitlab-org/gitlab/-/issues/333678 <- can cause 500 when creating user and group back to back
- it(
- 'imports group with subgroups and labels',
- testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1785'
- ) do
- Resource::GroupLabel.fabricate_via_api! do |label|
- label.api_client = api_client
- label.group = source_group
- label.title = "source-group-#{SecureRandom.hex(4)}"
- end
- Resource::GroupLabel.fabricate_via_api! do |label|
- label.api_client = api_client
- label.group = subgroup
- label.title = "subgroup-#{SecureRandom.hex(4)}"
- end
-
+ it 'imports group from UI', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1785' do
Page::Group::BulkImport.perform do |import_page|
- import_page.import_group(source_group.path, sandbox.path)
-
- expect(import_page).to have_imported_group(source_group.path, wait: 180)
+ import_page.import_group(imported_group.path, imported_group.sandbox.path)
- aggregate_failures do
- expect { imported_group.reload! }.to eventually_eq(source_group).within(duration: 10)
- expect { imported_group.labels }.to eventually_include(*source_group.labels).within(duration: 10)
+ expect(import_page).to have_imported_group(imported_group.path, wait: 300)
- # Do not validate subgroups until https://gitlab.com/gitlab-org/gitlab/-/issues/332818 is resolved
- # expect { imported_subgroup.reload! }.to eventually_eq(subgroup).within(duration: 30)
- # expect { imported_subgroup.labels }.to eventually_include(*subgroup.labels).within(duration: 30)
+ imported_group.reload!.visit!
+ Page::Group::Show.perform do |group|
+ expect(group).to have_content(imported_group.path)
end
end
end
diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb
index 4fffc786c14..564b14a872f 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb
@@ -2,24 +2,52 @@
module QA
RSpec.describe 'Manage', :smoke do
- describe 'Project creation' do
- it 'user creates a new project',
- testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1857' do
+ describe 'Project' do
+ shared_examples 'successful project creation' do
+ it 'creates a new project' do
+ Page::Project::Show.perform do |project|
+ expect(project).to have_content(project_name)
+ expect(project).to have_content(
+ /Project \S?#{project_name}\S+ was successfully created/
+ )
+ expect(project).to have_content('create awesome project test')
+ expect(project).to have_content('The repository for this project is empty')
+ end
+ end
+ end
+
+ before do
Flow::Login.sign_in
+ project
+ end
- created_project = Resource::Project.fabricate_via_browser_ui! do |project|
- project.name = 'awesome-project'
- project.description = 'create awesome project test'
+ context 'in group', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1857' do
+ let(:project_name) { "project-in-group-#{SecureRandom.hex(8)}" }
+ let(:project) do
+ Resource::Project.fabricate_via_browser_ui! do |project|
+ project.name = project_name
+ project.description = 'create awesome project test'
+ end
end
- Page::Project::Show.perform do |project|
- expect(project).to have_content(created_project.name)
- expect(project).to have_content(
- /Project \S?awesome-project\S+ was successfully created/
- )
- expect(project).to have_content('create awesome project test')
- expect(project).to have_content('The repository for this project is empty')
+ it_behaves_like 'successful project creation'
+ end
+
+ context 'in personal namespace', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1888' do
+ let(:project_name) { "project-in-personal-namespace-#{SecureRandom.hex(8)}" }
+ let(:project) do
+ Resource::Project.fabricate_via_browser_ui! do |project|
+ project.name = project_name
+ project.description = 'create awesome project test'
+ project.personal_namespace = true
+ end
end
+
+ it_behaves_like 'successful project creation'
+ end
+
+ after do
+ project.remove_via_api!
end
end
end
diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb
index 4f85fa257a2..c55ecb28361 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb
@@ -3,9 +3,11 @@
module QA
RSpec.describe 'Manage', :github, :requires_admin do
describe 'Project import' do
- let!(:api_client) { Runtime::API::Client.as_admin }
- let!(:group) { Resource::Group.fabricate_via_api! { |resource| resource.api_client = api_client } }
- let!(:user) do
+ let(:github_repo) { 'gitlab-qa-github/test-project' }
+ let(:imported_project_name) { 'imported-project' }
+ let(:api_client) { Runtime::API::Client.as_admin }
+ let(:group) { Resource::Group.fabricate_via_api! { |resource| resource.api_client = api_client } }
+ let(:user) do
Resource::User.fabricate_via_api! do |resource|
resource.api_client = api_client
resource.hard_delete_on_api_removal = true
@@ -13,16 +15,25 @@ module QA
end
let(:imported_project) do
- Resource::ProjectImportedFromGithub.fabricate_via_browser_ui! do |project|
- project.name = 'imported-project'
+ Resource::ProjectImportedFromGithub.init do |project|
+ project.import = true
+ project.add_name_uuid = false
+ project.name = imported_project_name
project.group = group
project.github_personal_access_token = Runtime::Env.github_access_token
- project.github_repository_path = 'gitlab-qa-github/test-project'
+ project.github_repository_path = github_repo
end
end
before do
group.add_member(user, Resource::Members::AccessLevel::MAINTAINER)
+
+ Flow::Login.sign_in(as: user)
+ Page::Main::Menu.perform(&:go_to_create_project)
+ Page::Project::New.perform do |project_page|
+ project_page.click_import_project
+ project_page.click_github_link
+ end
end
after do
@@ -30,13 +41,24 @@ module QA
end
it 'imports a GitHub repo', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1762' do
- Flow::Login.sign_in(as: user)
+ Page::Project::Import::Github.perform do |import_page|
+ import_page.add_personal_access_token(Runtime::Env.github_access_token)
+ import_page.import!(github_repo, group.full_path, imported_project_name)
- imported_project # import the project
+ aggregate_failures do
+ expect(import_page).to have_imported_project(github_repo)
+ # validate button is present instead of navigating to avoid dealing with multiple tabs
+ # which makes the test more complicated
+ expect(import_page).to have_go_to_project_button(github_repo)
+ end
+ end
+ imported_project.reload!.visit!
Page::Project::Show.perform do |project|
- expect(project).to have_content(imported_project.name)
- expect(project).to have_content('This test project is used for automated GitHub import by GitLab QA.')
+ aggregate_failures do
+ expect(project).to have_content(imported_project_name)
+ expect(project).to have_content('This test project is used for automated GitHub import by GitLab QA.')
+ end
end
end
end
diff --git a/qa/qa/specs/features/browser_ui/3_create/design_management/modify_design_content_spec.rb b/qa/qa/specs/features/browser_ui/3_create/design_management/modify_design_content_spec.rb
index 59d34612ca7..dfdc9b7c9b4 100644
--- a/qa/qa/specs/features/browser_ui/3_create/design_management/modify_design_content_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/design_management/modify_design_content_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
- RSpec.describe 'Create', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/331978', type: :bug } do
+ RSpec.describe 'Create' do
context 'Design Management' do
let(:design) do
Resource::Design.fabricate! do |design|
diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_diff_patch_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_diff_patch_spec.rb
index d11afde5648..37008e6d507 100644
--- a/qa/qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_diff_patch_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_diff_patch_spec.rb
@@ -15,7 +15,7 @@ module QA
merge_request.visit!
end
- it 'views the merge request email patches', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1689' do
+ it 'views the merge request email patches', :can_use_large_setup, testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1689' do
Page::MergeRequest::Show.perform(&:view_email_patches)
expect(page.text).to start_with('From')
@@ -23,10 +23,11 @@ module QA
expect(page).to have_content("diff --git a/#{merge_request.file_name} b/#{merge_request.file_name}")
end
- it 'views the merge request plain diff', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/417' do
+ it 'views the merge request plain diff', :can_use_large_setup, testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/417' do
Page::MergeRequest::Show.perform(&:view_plain_diff)
- expect(page.text).to start_with("diff --git a/#{merge_request.file_name} b/#{merge_request.file_name}")
+ expect(page.text).to start_with('diff')
+ expect(page).to have_content("diff --git a/#{merge_request.file_name} b/#{merge_request.file_name}")
expect(page).to have_content('+File Added')
end
end
diff --git a/qa/qa/specs/features/browser_ui/3_create/wiki/content_editor_spec.rb b/qa/qa/specs/features/browser_ui/3_create/wiki/content_editor_spec.rb
new file mode 100644
index 00000000000..2a46604f8ac
--- /dev/null
+++ b/qa/qa/specs/features/browser_ui/3_create/wiki/content_editor_spec.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module QA
+ RSpec.describe 'Create' do
+ context 'Content Editor' do
+ let(:initial_wiki) { Resource::Wiki::ProjectPage.fabricate_via_api! }
+ let(:page_title) { 'Content Editor Page' }
+ let(:heading_text) { 'My New Heading' }
+ let(:image_file_name) { 'testfile.png' }
+
+ before do
+ Flow::Login.sign_in
+ end
+
+ after do
+ initial_wiki.project.remove_via_api!
+ end
+
+ it 'creates a formatted Wiki page with an image uploaded', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1861' do
+ initial_wiki.visit!
+
+ Page::Project::Wiki::Show.perform(&:click_new_page)
+
+ Page::Project::Wiki::Edit.perform do |edit|
+ edit.set_title(page_title)
+ edit.use_new_editor
+ edit.add_heading('Heading 1', heading_text)
+ edit.upload_image(File.absolute_path(File.join('qa', 'fixtures', 'designs', image_file_name)))
+ end
+
+ Page::Project::Wiki::Edit.perform(&:click_submit)
+
+ Page::Project::Wiki::Show.perform do |wiki|
+ aggregate_failures 'page shows expected content' do
+ expect(wiki).to have_title(page_title)
+ expect(wiki).to have_heading('h1', heading_text)
+ expect(wiki).to have_image(image_file_name)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/browser_ui/4_verify/pipeline/include_local_config_file_paths_with_wildcard_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/pipeline/include_local_config_file_paths_with_wildcard_spec.rb
index 5083b7b0859..5b3949f9c3a 100644
--- a/qa/qa/specs/features/browser_ui/4_verify/pipeline/include_local_config_file_paths_with_wildcard_spec.rb
+++ b/qa/qa/specs/features/browser_ui/4_verify/pipeline/include_local_config_file_paths_with_wildcard_spec.rb
@@ -1,10 +1,8 @@
# frozen_string_literal: true
module QA
- RSpec.describe 'Verify', :requires_admin do
+ RSpec.describe 'Verify' do
describe 'Include local config file paths with wildcard' do
- let(:feature_flag) { :ci_wildcard_file_paths }
-
let(:project) do
Resource::Project.fabricate_via_api! do |project|
project.name = 'project-with-pipeline'
@@ -12,7 +10,6 @@ module QA
end
before do
- Runtime::Feature.enable(feature_flag)
Flow::Login.sign_in
add_files_to_project
project.visit!
@@ -20,7 +17,6 @@ module QA
end
after do
- Runtime::Feature.disable(feature_flag)
project.remove_via_api!
end
diff --git a/qa/qa/specs/features/browser_ui/4_verify/pipeline/merge_mr_when_pipline_is_blocked_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/pipeline/merge_mr_when_pipline_is_blocked_spec.rb
index 01aada2d6dd..b43581289ef 100644
--- a/qa/qa/specs/features/browser_ui/4_verify/pipeline/merge_mr_when_pipline_is_blocked_spec.rb
+++ b/qa/qa/specs/features/browser_ui/4_verify/pipeline/merge_mr_when_pipline_is_blocked_spec.rb
@@ -73,7 +73,7 @@ module QA
it 'can still merge MR successfully', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/971' do
Page::MergeRequest::Show.perform do |show|
# waiting for manual action status shows status badge 'blocked' on pipelines page
- show.wait_until(reload: false) { show.has_pipeline_status?('waiting for manual action') }
+ show.has_pipeline_status?('waiting for manual action')
show.merge!
expect(show).to be_merged
diff --git a/qa/qa/specs/features/browser_ui/4_verify/pipeline/mr_event_rule_pipeline_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/pipeline/mr_event_rule_pipeline_spec.rb
new file mode 100644
index 00000000000..47b36b55c8c
--- /dev/null
+++ b/qa/qa/specs/features/browser_ui/4_verify/pipeline/mr_event_rule_pipeline_spec.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+require 'faker'
+
+module QA
+ RSpec.describe 'Verify', :runner do
+ context 'When job is configured to only run on merge_request_events' do
+ let(:mr_only_job_name) { 'mr_only_job' }
+ let(:non_mr_only_job_name) { 'non_mr_only_job' }
+ let(:executor) { "qa-runner-#{Faker::Alphanumeric.alphanumeric(8)}" }
+
+ let(:project) do
+ Resource::Project.fabricate_via_api! do |project|
+ project.name = 'merge-request-only-job'
+ end
+ end
+
+ let!(:runner) do
+ Resource::Runner.fabricate! do |runner|
+ runner.project = project
+ runner.name = executor
+ runner.tags = [executor]
+ end
+ end
+
+ let!(:ci_file) do
+ Resource::Repository::Commit.fabricate_via_api! do |commit|
+ commit.project = project
+ commit.commit_message = 'Add .gitlab-ci.yml'
+ commit.add_files(
+ [
+ {
+ file_path: '.gitlab-ci.yml',
+ content: <<~YAML
+ #{mr_only_job_name}:
+ script: echo 'OK'
+ rules:
+ - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
+ #{non_mr_only_job_name}:
+ script: echo 'OK'
+ rules:
+ - if: '$CI_PIPELINE_SOURCE != "merge_request_event"'
+ YAML
+ }
+ ]
+ )
+ end
+ end
+
+ let(:merge_request) do
+ Resource::MergeRequest.fabricate_via_api! do |merge_request|
+ merge_request.project = project
+ merge_request.description = Faker::Lorem.sentence
+ merge_request.target_new_branch = false
+ merge_request.file_name = 'new.txt'
+ merge_request.file_content = Faker::Lorem.sentence
+ end
+ end
+
+ before do
+ Flow::Login.sign_in
+ merge_request.visit!
+ Page::MergeRequest::Show.perform(&:click_pipeline_link)
+ end
+
+ after do
+ runner.remove_via_api!
+ project.remove_via_api!
+ end
+
+ it 'only runs the job configured to run on merge requests', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/170' do
+ Page::Project::Pipeline::Show.perform do |pipeline|
+ aggregate_failures do
+ expect(pipeline).to have_job(mr_only_job_name)
+ expect(pipeline).to have_no_job(non_mr_only_job_name)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/browser_ui/4_verify/pipeline/pass_dotenv_variables_to_downstream_via_bridge_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/pipeline/pass_dotenv_variables_to_downstream_via_bridge_spec.rb
index c1625f1e679..adacedb36ab 100644
--- a/qa/qa/specs/features/browser_ui/4_verify/pipeline/pass_dotenv_variables_to_downstream_via_bridge_spec.rb
+++ b/qa/qa/specs/features/browser_ui/4_verify/pipeline/pass_dotenv_variables_to_downstream_via_bridge_spec.rb
@@ -41,7 +41,7 @@ module QA
after do
runner.remove_via_api!
- group.remove_via_api!
+ [upstream_project, downstream_project].each(&:remove_via_api!)
end
it 'runs the pipeline with composed config', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1086' do
diff --git a/qa/qa/specs/features/browser_ui/4_verify/testing/view_code_coverage_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/testing/view_code_coverage_spec.rb
index 07484feb686..7d3f8f2b1d4 100644
--- a/qa/qa/specs/features/browser_ui/4_verify/testing/view_code_coverage_spec.rb
+++ b/qa/qa/specs/features/browser_ui/4_verify/testing/view_code_coverage_spec.rb
@@ -39,9 +39,7 @@ module QA
merge_request.visit!
Page::MergeRequest::Show.perform do |mr_widget|
- Support::Retrier.retry_until(max_attempts: 5, sleep_interval: 5) do
- mr_widget.has_pipeline_status?('passed')
- end
+ mr_widget.has_pipeline_status?('passed')
expect(mr_widget).to have_content('Test coverage 66.67%')
end
end
diff --git a/qa/qa/specs/features/browser_ui/5_package/container_registry_omnibus_spec.rb b/qa/qa/specs/features/browser_ui/5_package/container_registry_omnibus_spec.rb
index 4b7669810ec..375a371c2b1 100644
--- a/qa/qa/specs/features/browser_ui/5_package/container_registry_omnibus_spec.rb
+++ b/qa/qa/specs/features/browser_ui/5_package/container_registry_omnibus_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
- RSpec.describe 'Package', :registry, :orchestrated do
+ RSpec.describe 'Package', :registry, :orchestrated, only: { pipeline: :main } do
describe 'Self-managed Container Registry' do
let(:project) do
Resource::Project.fabricate_via_api! do |project|
diff --git a/qa/qa/specs/features/browser_ui/5_package/dependency_proxy_spec.rb b/qa/qa/specs/features/browser_ui/5_package/dependency_proxy_spec.rb
new file mode 100644
index 00000000000..be1d0dd8e81
--- /dev/null
+++ b/qa/qa/specs/features/browser_ui/5_package/dependency_proxy_spec.rb
@@ -0,0 +1,104 @@
+# frozen_string_literal: true
+
+module QA
+ RSpec.describe 'Package', :orchestrated, :registry do
+ describe 'Dependency Proxy' do
+ let(:project) do
+ Resource::Project.fabricate_via_api! do |project|
+ project.name = 'dependency-proxy-project'
+ project.visibility = :private
+ end
+ end
+
+ let!(:runner) do
+ Resource::Runner.fabricate! do |runner|
+ runner.name = "qa-runner-#{Time.now.to_i}"
+ runner.tags = ["runner-for-#{project.name}"]
+ runner.executor = :docker
+ runner.project = project
+ end
+ end
+
+ let(:uri) { URI.parse(Runtime::Scenario.gitlab_address) }
+ let(:gitlab_host_with_port) { "#{uri.host}:#{uri.port}" }
+ let(:dependency_proxy_url) { "#{gitlab_host_with_port}/#{project.group.full_path}/dependency_proxy/containers" }
+
+ before do
+ Flow::Login.sign_in
+
+ project.group.visit!
+
+ Page::Group::Menu.perform(&:go_to_dependency_proxy)
+
+ Page::Group::DependencyProxy.perform do |index|
+ expect(index).to have_dependency_proxy_enabled
+ end
+ end
+
+ after do
+ runner.remove_via_api!
+ end
+
+ where(:docker_client_version) do
+ %w[docker:19.03.12 docker:20.10]
+ end
+
+ with_them do
+ it "pulls an image using the dependency proxy", testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1862' do
+ Resource::Repository::Commit.fabricate_via_api! do |commit|
+ commit.project = project
+ commit.commit_message = 'Add .gitlab-ci.yml'
+ commit.add_files([{
+ file_path: '.gitlab-ci.yml',
+ content:
+ <<~YAML
+ dependency-proxy-pull-test:
+ image: "#{docker_client_version}"
+ services:
+ - name: "#{docker_client_version}-dind"
+ command:
+ - /bin/sh
+ - -c
+ - |
+ apk add --no-cache openssl
+ true | openssl s_client -showcerts -connect gitlab.test:5050 > /usr/local/share/ca-certificates/gitlab.test.crt
+ update-ca-certificates
+ dockerd-entrypoint.sh || exit
+ before_script:
+ - apk add curl jq grep
+ - docker login -u "$CI_DEPENDENCY_PROXY_USER" -p "$CI_DEPENDENCY_PROXY_PASSWORD" "$CI_DEPENDENCY_PROXY_SERVER"
+ script:
+ - docker pull #{dependency_proxy_url}/alpine:latest
+ - TOKEN=$(curl "https://auth.docker.io/token?service=registry.docker.io&scope=repository:ratelimitpreview/test:pull" | jq --raw-output .token)
+ - 'curl --head --header "Authorization: Bearer $TOKEN" "https://registry-1.docker.io/v2/ratelimitpreview/test/manifests/latest" 2>&1'
+ - docker pull #{dependency_proxy_url}/alpine:latest
+ - 'curl --head --header "Authorization: Bearer $TOKEN" "https://registry-1.docker.io/v2/ratelimitpreview/test/manifests/latest" 2>&1'
+ tags:
+ - "runner-for-#{project.name}"
+ YAML
+ }])
+ end
+
+ project.visit!
+ Flow::Pipeline.visit_latest_pipeline
+
+ Page::Project::Pipeline::Show.perform do |pipeline|
+ pipeline.click_job('dependency-proxy-pull-test')
+ end
+
+ Page::Project::Job::Show.perform do |job|
+ expect(job).to be_successful(timeout: 800)
+ end
+
+ project.group.visit!
+
+ Page::Group::Menu.perform(&:go_to_dependency_proxy)
+
+ Page::Group::DependencyProxy.perform do |index|
+ expect(index).to have_blob_count("Contains 2 blobs of images")
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/browser_ui/5_package/online_garbage_collection_spec.rb b/qa/qa/specs/features/browser_ui/5_package/online_garbage_collection_spec.rb
index 65fc12545b7..8c686e65e33 100644
--- a/qa/qa/specs/features/browser_ui/5_package/online_garbage_collection_spec.rb
+++ b/qa/qa/specs/features/browser_ui/5_package/online_garbage_collection_spec.rb
@@ -2,7 +2,7 @@
module QA
RSpec.describe 'Package' do
- describe 'Container Registry Online Garbage Collection', :registry_gc, only: { subdomain: %i[pre] } do
+ describe 'Container Registry Online Garbage Collection', :registry_gc, only: { subdomain: %i[pre] }, quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/337791', type: :waiting_on } do
let(:group) { Resource::Group.fabricate_via_api! }
let(:imported_project) do
diff --git a/qa/qa/specs/features/browser_ui/5_package/pypi_repository_spec.rb b/qa/qa/specs/features/browser_ui/5_package/pypi_repository_spec.rb
index a9034174cab..7b924f1b52b 100644
--- a/qa/qa/specs/features/browser_ui/5_package/pypi_repository_spec.rb
+++ b/qa/qa/specs/features/browser_ui/5_package/pypi_repository_spec.rb
@@ -26,10 +26,10 @@ module QA
end
end
- let(:gitlab_address_with_port) do
- uri = URI.parse(Runtime::Scenario.gitlab_address)
- "#{uri.scheme}://#{uri.host}:#{uri.port}"
- end
+ let(:uri) { URI.parse(Runtime::Scenario.gitlab_address) }
+ let(:gitlab_address_with_port) { "#{uri.scheme}://#{uri.host}:#{uri.port}" }
+ let(:gitlab_host_with_port) { "#{uri.host}:#{uri.port}" }
+ let(:personal_access_token) { Runtime::Env.personal_access_token }
before do
Flow::Login.sign_in
@@ -42,14 +42,25 @@ module QA
content:
<<~YAML
image: python:latest
+ stages:
+ - run
+ - install
run:
+ stage: run
script:
- pip install twine
- python setup.py sdist bdist_wheel
- "TWINE_PASSWORD=${CI_JOB_TOKEN} TWINE_USERNAME=gitlab-ci-token python -m twine upload --repository-url #{gitlab_address_with_port}/api/v4/projects/${CI_PROJECT_ID}/packages/pypi dist/*"
tags:
- "runner-for-#{project.name}"
+ install:
+ stage: install
+ script:
+ - "pip install mypypipackage --no-deps --index-url #{uri.scheme}://#{personal_access_token}:#{personal_access_token}@#{gitlab_host_with_port}/api/v4/projects/${CI_PROJECT_ID}/packages/pypi/simple --trusted-host #{gitlab_host_with_port}"
+ tags:
+ - "runner-for-#{project.name}"
+
YAML
},
{
@@ -87,6 +98,16 @@ module QA
Page::Project::Job::Show.perform do |job|
expect(job).to be_successful(timeout: 800)
end
+
+ Flow::Pipeline.visit_latest_pipeline
+
+ Page::Project::Pipeline::Show.perform do |pipeline|
+ pipeline.click_job('install')
+ end
+
+ Page::Project::Job::Show.perform do |job|
+ expect(job).to be_successful(timeout: 800)
+ end
end
after do
@@ -95,20 +116,22 @@ module QA
project&.remove_via_api!
end
- it 'publishes a pypi package and deletes it', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1087' do
- Page::Project::Menu.perform(&:click_packages_link)
+ context 'when at the project level' do
+ it 'publishes and installs a pypi package and deletes it', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1087' do
+ Page::Project::Menu.perform(&:click_packages_link)
- Page::Project::Packages::Index.perform do |index|
- expect(index).to have_package(package.name)
- index.click_package(package.name)
- end
+ Page::Project::Packages::Index.perform do |index|
+ expect(index).to have_package(package.name)
+ index.click_package(package.name)
+ end
- Page::Project::Packages::Show.perform(&:click_delete)
+ Page::Project::Packages::Show.perform(&:click_delete)
- Page::Project::Packages::Index.perform do |index|
- aggregate_failures do
- expect(index).to have_content("Package deleted successfully")
- expect(index).not_to have_package(package.name)
+ Page::Project::Packages::Index.perform do |index|
+ aggregate_failures do
+ expect(index).to have_content("Package deleted successfully")
+ expect(index).not_to have_package(package.name)
+ end
end
end
end
diff --git a/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb b/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb
index 8e61ec4d2d7..f4a5c715684 100644
--- a/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb
+++ b/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb
@@ -5,27 +5,31 @@ require 'digest/sha1'
module QA
RSpec.describe 'Release', :runner do
describe 'Git clone using a deploy key' do
- before do
- Flow::Login.sign_in
-
- @runner_name = "qa-runner-#{Time.now.to_i}"
+ let(:runner_name) { "qa-runner-#{SecureRandom.hex(4)}" }
+ let(:repository_location) { project.repository_ssh_location }
- @project = Resource::Project.fabricate_via_api! do |project|
+ let(:project) do
+ Resource::Project.fabricate_via_api! do |project|
project.name = 'deploy-key-clone-project'
end
+ end
- @repository_location = @project.repository_ssh_location
-
- @runner = Resource::Runner.fabricate_via_api! do |resource|
- resource.project = @project
- resource.name = @runner_name
- resource.tags = %w[qa docker]
+ let!(:runner) do
+ Resource::Runner.fabricate_via_api! do |resource|
+ resource.project = project
+ resource.name = runner_name
+ resource.tags = [runner_name]
resource.image = 'gitlab/gitlab-runner:alpine'
end
end
+ before do
+ Flow::Login.sign_in
+ end
+
after do
- @runner.remove_via_api!
+ runner.remove_via_api!
+ project.remove_via_api!
end
keys = [
@@ -39,7 +43,7 @@ module QA
key = key_class.new(*bits)
Resource::DeployKey.fabricate_via_browser_ui! do |resource|
- resource.project = @project
+ resource.project = project
resource.title = "deploy key #{key.name}(#{key.bits})"
resource.key = key.public_key
end
@@ -49,25 +53,23 @@ module QA
make_ci_variable(deploy_key_name, key)
gitlab_ci = <<~YAML
- cat-config:
- script:
- - apk add --update --no-cache openssh-client
- - mkdir -p ~/.ssh
- - ssh-keyscan -p #{@repository_location.port} #{@repository_location.host} >> ~/.ssh/known_hosts
- - eval $(ssh-agent -s)
- - ssh-add -D
- - echo "$#{deploy_key_name}" | ssh-add -
- - git clone #{@repository_location.git_uri}
- - cd #{@project.name}
- - git checkout #{deploy_key_name}
- - sha1sum .gitlab-ci.yml
- tags:
- - qa
- - docker
+ cat-config:
+ script:
+ - which ssh-agent || ( apk --update add openssh-client )
+ - mkdir -p ~/.ssh
+ - ssh-keyscan -p #{repository_location.port} #{repository_location.host} >> ~/.ssh/known_hosts
+ - eval $(ssh-agent -s)
+ - ssh-add -D
+ - echo "$#{deploy_key_name}" | ssh-add -
+ - git clone #{repository_location.git_uri}
+ - cd #{project.name}
+ - git checkout #{deploy_key_name}
+ - sha1sum .gitlab-ci.yml
+ tags: [#{runner_name}]
YAML
Resource::Repository::ProjectPush.fabricate! do |resource|
- resource.project = @project
+ resource.project = project
resource.file_name = '.gitlab-ci.yml'
resource.commit_message = 'Add .gitlab-ci.yml'
resource.file_content = gitlab_ci
@@ -81,8 +83,10 @@ module QA
Page::Project::Pipeline::Show.perform(&:click_on_first_job)
Page::Project::Job::Show.perform do |job|
- expect(job).to be_successful
- expect(job.output).to include(sha1sum)
+ aggregate_failures 'job succeeds and has correct sha1sum' do
+ expect(job).to be_successful
+ expect(job.output).to include(sha1sum)
+ end
end
end
@@ -90,7 +94,7 @@ module QA
def make_ci_variable(key_name, key)
Resource::CiVariable.fabricate_via_api! do |resource|
- resource.project = @project
+ resource.project = project
resource.key = key_name
resource.value = key.private_key
resource.masked = false
diff --git a/qa/qa/specs/features/browser_ui/7_configure/kubernetes/kubernetes_integration_spec.rb b/qa/qa/specs/features/browser_ui/7_configure/kubernetes/kubernetes_integration_spec.rb
index 1803b4b16de..3a59efe645a 100644
--- a/qa/qa/specs/features/browser_ui/7_configure/kubernetes/kubernetes_integration_spec.rb
+++ b/qa/qa/specs/features/browser_ui/7_configure/kubernetes/kubernetes_integration_spec.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
module QA
- RSpec.describe 'Configure' do
- describe 'Kubernetes Cluster Integration', :orchestrated, :kubernetes, :requires_admin, :skip_live_env, quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/225315', type: :flaky } do
+ RSpec.describe 'Configure', except: { job: 'review-qa-*' } do
+ describe 'Kubernetes Cluster Integration', :requires_admin, :skip_live_env, :smoke do
context 'Project Clusters' do
let!(:cluster) { Service::KubernetesCluster.new(provider_class: Service::ClusterProvider::K3s).create! }
let(:project) do
@@ -20,7 +20,7 @@ module QA
cluster.remove!
end
- it 'can create and associate a project cluster', :smoke, testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/707' do
+ it 'can create and associate a project cluster', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/707' do
Resource::KubernetesCluster::ProjectCluster.fabricate_via_browser_ui! do |k8s_cluster|
k8s_cluster.project = project
k8s_cluster.cluster = cluster
diff --git a/qa/qa/specs/helpers/context_formatter.rb b/qa/qa/specs/helpers/context_formatter.rb
new file mode 100644
index 00000000000..26db7c3b67e
--- /dev/null
+++ b/qa/qa/specs/helpers/context_formatter.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+require 'rspec/core'
+require "rspec/core/formatters/base_formatter"
+
+module QA
+ module Specs
+ module Helpers
+ class ContextFormatter < ::RSpec::Core::Formatters::BaseFormatter
+ include ContextSelector
+
+ ::RSpec::Core::Formatters.register(
+ self,
+ :example_group_started,
+ :example_started
+ )
+
+ # Starts example group
+ # @param [RSpec::Core::Notifications::GroupNotification] example_group_notification
+ # @return [void]
+ def example_group_started(example_group_notification)
+ set_skip_metadata(example_group_notification.group)
+ end
+
+ # Starts example
+ # @param [RSpec::Core::Notifications::ExampleNotification] example_notification
+ # @return [void]
+ def example_started(example_notification)
+ example = example_notification.example
+
+ # if skip propagated from example_group, do not reset skip metadata
+ set_skip_metadata(example_notification.example) unless example.metadata[:skip]
+ end
+
+ private
+
+ # Skip example_group or example
+ #
+ # @param [<RSpec::Core::ExampleGroup, RSpec::Core::Example>] example
+ # @return [void]
+ def set_skip_metadata(example)
+ return skip_only(example.metadata) if example.metadata.key?(:only)
+ return skip_except(example.metadata) if example.metadata.key?(:except)
+ end
+
+ # Skip based on 'only' condition
+ #
+ # @param [Hash] metadata
+ # @return [void]
+ def skip_only(metadata)
+ return if context_matches?(metadata[:only])
+
+ metadata[:skip] = 'Test is not compatible with this environment or pipeline'
+ end
+
+ # Skip based on 'except' condition
+ #
+ # @param [Hash] metadata
+ # @return [void]
+ def skip_except(metadata)
+ return unless except?(metadata[:except])
+
+ metadata[:skip] = 'Test is excluded in this job'
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/helpers/context_selector.rb b/qa/qa/specs/helpers/context_selector.rb
index 40ecb9b3506..57665babf68 100644
--- a/qa/qa/specs/helpers/context_selector.rb
+++ b/qa/qa/specs/helpers/context_selector.rb
@@ -8,18 +8,6 @@ module QA
module ContextSelector
extend self
- def configure_rspec
- ::RSpec.configure do |config|
- config.before do |example|
- if example.metadata.key?(:only)
- skip('Test is not compatible with this environment or pipeline') unless ContextSelector.context_matches?(example.metadata[:only])
- elsif example.metadata.key?(:except)
- skip('Test is excluded in this job') if ContextSelector.except?(example.metadata[:except])
- end
- end
- end
- end
-
def except?(*options)
return false if Runtime::Env.ci_job_name.blank? && options.any? { |o| o.is_a?(Hash) && o[:job].present? }
return false if Runtime::Env.ci_project_name.blank? && options.any? { |o| o.is_a?(Hash) && o[:pipeline].present? }
diff --git a/qa/qa/specs/helpers/quarantine.rb b/qa/qa/specs/helpers/quarantine.rb
index 15b4ed8336b..49d91fc87cd 100644
--- a/qa/qa/specs/helpers/quarantine.rb
+++ b/qa/qa/specs/helpers/quarantine.rb
@@ -10,26 +10,6 @@ module QA
extend self
- def configure_rspec
- ::RSpec.configure do |config|
- config.before(:context, :quarantine) do
- Quarantine.skip_or_run_quarantined_contexts(config.inclusion_filter.rules, self.class)
- end
-
- config.before do |example|
- Quarantine.skip_or_run_quarantined_tests_or_contexts(config.inclusion_filter.rules, example)
- end
- end
- end
-
- # Skip the entire context if a context is quarantined. This avoids running
- # before blocks unnecessarily.
- def skip_or_run_quarantined_contexts(filters, example)
- return unless example.metadata.key?(:quarantine)
-
- skip_or_run_quarantined_tests_or_contexts(filters, example)
- end
-
# Skip tests in quarantine unless we explicitly focus on them.
def skip_or_run_quarantined_tests_or_contexts(filters, example)
if filters.key?(:quarantine)
@@ -43,19 +23,19 @@ module QA
# running that ldap test as well because of the :quarantine metadata.
# We could use an exclusion filter, but this way the test report will list
# the quarantined tests when they're not run so that we're aware of them
- skip("Only running tests tagged with :quarantine and any of #{included_filters.keys}") if should_skip_when_focused?(example.metadata, included_filters)
- else
- if example.metadata.key?(:quarantine)
- quarantine_tag = example.metadata[:quarantine]
-
- if quarantine_tag.is_a?(Hash) && quarantine_tag&.key?(:only)
- # If the :quarantine hash contains :only, we respect that.
- # For instance `quarantine: { only: { subdomain: :staging } }` will only quarantine the test when it runs against staging.
- return unless ContextSelector.context_matches?(quarantine_tag[:only])
- end
+ if should_skip_when_focused?(example.metadata, included_filters)
+ example.metadata[:skip] = "Only running tests tagged with :quarantine and any of #{included_filters.keys}"
+ end
+ elsif example.metadata.key?(:quarantine)
+ quarantine_tag = example.metadata[:quarantine]
- skip(quarantine_message(quarantine_tag))
+ if quarantine_tag.is_a?(Hash) && quarantine_tag&.key?(:only) && !ContextSelector.context_matches?(quarantine_tag[:only])
+ # If the :quarantine hash contains :only, we respect that.
+ # For instance `quarantine: { only: { subdomain: :staging } }` will only quarantine the test when it runs against staging.
+ return
end
+
+ example.metadata[:skip] = quarantine_message(quarantine_tag)
end
end
@@ -64,7 +44,7 @@ module QA
end
def quarantine_message(quarantine_tag)
- quarantine_message = %w(In quarantine)
+ quarantine_message = %w[In quarantine]
quarantine_message << case quarantine_tag
when String
": #{quarantine_tag}"
diff --git a/qa/qa/specs/helpers/quarantine_formatter.rb b/qa/qa/specs/helpers/quarantine_formatter.rb
new file mode 100644
index 00000000000..c42debee07c
--- /dev/null
+++ b/qa/qa/specs/helpers/quarantine_formatter.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'rspec/core'
+require "rspec/core/formatters/base_formatter"
+
+module QA
+ module Specs
+ module Helpers
+ class QuarantineFormatter < ::RSpec::Core::Formatters::BaseFormatter
+ include Quarantine
+
+ ::RSpec::Core::Formatters.register(
+ self,
+ :example_group_started,
+ :example_started
+ )
+
+ # Starts example group
+ # @param [RSpec::Core::Notifications::GroupNotification] example_group_notification
+ # @return [void]
+ def example_group_started(example_group_notification)
+ group = example_group_notification.group
+
+ skip_or_run_quarantined_tests_or_contexts(filters, group)
+ end
+
+ # Starts example
+ # @param [RSpec::Core::Notifications::ExampleNotification] example_notification
+ # @return [void]
+ def example_started(example_notification)
+ example = example_notification.example
+
+ # if skip propagated from example_group, do not reset skip metadata
+ skip_or_run_quarantined_tests_or_contexts(filters, example) unless example.metadata[:skip]
+ end
+
+ private
+
+ def filters
+ @filters ||= ::RSpec.configuration.inclusion_filter.rules
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/helpers/rspec.rb b/qa/qa/specs/helpers/rspec.rb
index f49e556b0d9..853dfbfd1b6 100644
--- a/qa/qa/specs/helpers/rspec.rb
+++ b/qa/qa/specs/helpers/rspec.rb
@@ -19,8 +19,10 @@ module QA
# expanding into the global state
# See: https://github.com/rspec/rspec-core/issues/2603
def describe_successfully(*args, &describe_body)
+ reporter = ::RSpec.configuration.reporter
+
example_group = RSpec.describe(*args, &describe_body)
- ran_successfully = example_group.run RaiseOnFailuresReporter
+ ran_successfully = example_group.run reporter
expect(ran_successfully).to eq true
example_group
end
diff --git a/qa/qa/support/allure_metadata_formatter.rb b/qa/qa/support/allure_metadata_formatter.rb
new file mode 100644
index 00000000000..8a18eeca839
--- /dev/null
+++ b/qa/qa/support/allure_metadata_formatter.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require 'rspec/core'
+require "rspec/core/formatters/base_formatter"
+
+module QA
+ module Support
+ class AllureMetadataFormatter < ::RSpec::Core::Formatters::BaseFormatter
+ ::RSpec::Core::Formatters.register(
+ self,
+ :example_started
+ )
+
+ # Starts example
+ # @param [RSpec::Core::Notifications::ExampleNotification] example_notification
+ # @return [void]
+ def example_started(example_notification)
+ example = example_notification.example
+
+ testcase = example.metadata[:testcase]
+ example.tms('Testcase', testcase) if testcase
+
+ quarantine_issue = example.metadata.dig(:quarantine, :issue)
+ example.issue('Quarantine issue', quarantine_issue) if quarantine_issue
+
+ spec_file = example.file_path.split('/').last
+ example.issue(
+ 'Failure issues',
+ "https://gitlab.com/gitlab-org/gitlab/-/issues?scope=all&state=opened&search=#{spec_file}"
+ )
+ return unless Runtime::Env.running_in_ci?
+
+ example.add_link(name: "Job(#{Runtime::Env.ci_job_name})", url: Runtime::Env.ci_job_url)
+ end
+ end
+ end
+end
diff --git a/qa/qa/support/api.rb b/qa/qa/support/api.rb
index de9da3171b0..579227b4f7a 100644
--- a/qa/qa/support/api.rb
+++ b/qa/qa/support/api.rb
@@ -79,11 +79,31 @@ module QA
error.response
end
- def with_paginated_response_body(url)
+ def auto_paginated_response(url, attempts: 0)
+ pages = []
+ with_paginated_response_body(url, attempts: attempts) { |response| pages << response }
+
+ pages.flatten
+ end
+
+ def with_paginated_response_body(url, attempts: 0)
+ not_ok_error = lambda do |resp|
+ raise "Failed to GET #{QA::Runtime::API::Request.masked_url(url)} - (#{resp.code}): `#{resp}`."
+ end
+
loop do
- response = get(url)
+ response = if attempts > 0
+ Retrier.retry_on_exception(max_attempts: attempts, log: false) do
+ get(url).tap { |resp| not_ok_error.call(resp) if resp.code != HTTP_STATUS_OK }
+ end
+ else
+ get(url).tap { |resp| not_ok_error.call(resp) if resp.code != HTTP_STATUS_OK }
+ end
+
+ page, pages = response.headers.values_at(:x_page, :x_total_pages)
+ api_endpoint = url.match(%r{v4/(\S+)\?})[1]
- QA::Runtime::Logger.debug("Fetching page #{response.headers[:x_page]} of #{response.headers[:x_total_pages]}...")
+ QA::Runtime::Logger.debug("Fetching page (#{page}/#{pages}) for '#{api_endpoint}' ...") unless pages.to_i <= 1
yield parse_body(response)
@@ -95,8 +115,11 @@ module QA
end
def pagination_links(response)
- response.headers[:link].split(',').map do |link|
- match = link.match(/\<(?<url>.*)\>\; rel=\"(?<rel>\w+)\"/)
+ link = response.headers[:link]
+ return unless link
+
+ link.split(',').map do |link|
+ match = link.match(/<(?<url>.*)>; rel="(?<rel>\w+)"/)
break nil unless match
{ url: match[:url], rel: match[:rel] }
diff --git a/qa/qa/support/repeater.rb b/qa/qa/support/repeater.rb
index 6f8c4a59566..b3a2472d702 100644
--- a/qa/qa/support/repeater.rb
+++ b/qa/qa/support/repeater.rb
@@ -11,7 +11,15 @@ module QA
RetriesExceededError = Class.new(RepeaterConditionExceededError)
WaitExceededError = Class.new(RepeaterConditionExceededError)
- def repeat_until(max_attempts: nil, max_duration: nil, reload_page: nil, sleep_interval: 0, raise_on_failure: true, retry_on_exception: false, log: true)
+ def repeat_until(
+ max_attempts: nil,
+ max_duration: nil,
+ reload_page: nil,
+ sleep_interval: 0,
+ raise_on_failure: true,
+ retry_on_exception: false,
+ log: true
+ )
attempts = 0
start = Time.now
@@ -29,17 +37,19 @@ module QA
raise unless retry_on_exception
attempts += 1
- if remaining_attempts?(attempts, max_attempts) && remaining_time?(start, max_duration)
- sleep_and_reload_if_needed(sleep_interval, reload_page)
+ raise unless remaining_attempts?(attempts, max_attempts) && remaining_time?(start, max_duration)
- retry
- else
- raise
- end
+ sleep_and_reload_if_needed(sleep_interval, reload_page)
+ retry
end
if raise_on_failure
- raise RetriesExceededError, "Retry condition not met after #{max_attempts} #{'attempt'.pluralize(max_attempts)}" unless remaining_attempts?(attempts, max_attempts)
+ unless remaining_attempts?(attempts, max_attempts)
+ raise(
+ RetriesExceededError,
+ "Retry condition not met after #{max_attempts} #{'attempt'.pluralize(max_attempts)}"
+ )
+ end
raise WaitExceededError, "Wait condition not met after #{max_duration} #{'second'.pluralize(max_duration)}"
end
diff --git a/qa/qa/support/retrier.rb b/qa/qa/support/retrier.rb
index 25dbb42cf6f..fde8ac263ca 100644
--- a/qa/qa/support/retrier.rb
+++ b/qa/qa/support/retrier.rb
@@ -7,21 +7,21 @@ module QA
module_function
- def retry_on_exception(max_attempts: 3, reload_page: nil, sleep_interval: 0.5)
- QA::Runtime::Logger.debug(
- <<~MSG.tr("\n", ' ')
- with retry_on_exception: max_attempts: #{max_attempts};
- reload_page: #{reload_page};
- sleep_interval: #{sleep_interval}
- MSG
- )
+ def retry_on_exception(max_attempts: 3, reload_page: nil, sleep_interval: 0.5, log: true)
+ if log
+ msg = ["with retry_on_exception: max_attempts: #{max_attempts}"]
+ msg << "reload_page: #{reload_page}" if reload_page
+ msg << "sleep_interval: #{sleep_interval}"
+ QA::Runtime::Logger.debug(msg.join('; '))
+ end
result = nil
repeat_until(
max_attempts: max_attempts,
reload_page: reload_page,
sleep_interval: sleep_interval,
- retry_on_exception: true
+ retry_on_exception: true,
+ log: log
) do
result = yield
@@ -29,7 +29,7 @@ module QA
# We set it to `true` so that it doesn't repeat if there's no exception
true
end
- QA::Runtime::Logger.debug("ended retry_on_exception")
+ QA::Runtime::Logger.debug("ended retry_on_exception") if log
result
end
diff --git a/qa/spec/page/logging_spec.rb b/qa/spec/page/logging_spec.rb
index df3447770be..3e1011dcd2a 100644
--- a/qa/spec/page/logging_spec.rb
+++ b/qa/spec/page/logging_spec.rb
@@ -12,6 +12,7 @@ RSpec.describe QA::Support::Page::Logging do
QA::Runtime::Logger.logger = logger
allow(Capybara).to receive(:current_session).and_return(page)
+ allow(page).to receive(:find).and_return(page)
allow(page).to receive(:current_url).and_return('http://current-url')
allow(page).to receive(:has_css?).with(any_args).and_return(true)
end
diff --git a/qa/spec/qa_deprecation_toolkit_env.rb b/qa/spec/qa_deprecation_toolkit_env.rb
new file mode 100644
index 00000000000..cdd5d954b20
--- /dev/null
+++ b/qa/spec/qa_deprecation_toolkit_env.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require 'deprecation_toolkit'
+require 'deprecation_toolkit/rspec'
+require 'concurrent/utility/monotonic_time'
+require 'active_support/gem_version'
+
+module QaDeprecationToolkitEnv
+ # Taken from https://github.com/jeremyevans/ruby-warning/blob/1.1.0/lib/warning.rb#L18
+ def self.kwargs_warning
+ %r{warning: (?:Using the last argument (?:for `.+' )?as keyword parameters is deprecated; maybe \*\* should be added to the call|Passing the keyword argument (?:for `.+' )?as the last hash parameter is deprecated|Splitting the last argument (?:for `.+' )?into positional and keyword parameters is deprecated|The called method (?:`.+' )?is defined here)\n\z}
+ end
+
+ def self.configure!
+ # Enable ruby deprecations for keywords, it's suppressed by default in Ruby 2.7.2
+ Warning[:deprecated] = true
+
+ DeprecationToolkit::Configuration.test_runner = :rspec
+ DeprecationToolkit::Configuration.deprecation_path = 'deprecations'
+ DeprecationToolkit::Configuration.warnings_treated_as_deprecation = [kwargs_warning]
+ end
+end
diff --git a/qa/spec/resource/base_spec.rb b/qa/spec/resource/base_spec.rb
index c0bedf794be..c6dd56b5f47 100644
--- a/qa/spec/resource/base_spec.rb
+++ b/qa/spec/resource/base_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe QA::Resource::Base do
let(:resource) { spy('resource') }
let(:location) { 'http://location' }
- shared_context 'fabrication context' do
+ shared_context 'with fabrication context' do
subject do
Class.new(described_class) do
def self.name
@@ -28,24 +28,14 @@ RSpec.describe QA::Resource::Base do
expect(resource).to receive(:something!).ordered
expect(resource).to receive(fabrication_method_used).ordered.and_return(location)
- subject.public_send(fabrication_method_called, resource: resource) do |resource|
- resource.something!
- end
- end
-
- it 'does not log the resource and build method when QA_DEBUG=false' do
- stub_env('QA_DEBUG', 'false')
- expect(resource).to receive(fabrication_method_used).and_return(location)
-
- expect { subject.public_send(fabrication_method_called, 'something', resource: resource) }
- .not_to output.to_stdout
+ subject.public_send(fabrication_method_called, resource: resource, &:something!)
end
end
describe '.fabricate!' do
context 'when resource does not support fabrication via the API' do
before do
- expect(described_class).to receive(:fabricate_via_api!).and_raise(NotImplementedError)
+ allow(described_class).to receive(:fabricate_via_api!).and_raise(NotImplementedError)
end
it 'calls .fabricate_via_browser_ui!' do
@@ -65,7 +55,7 @@ RSpec.describe QA::Resource::Base do
end
describe '.fabricate_via_api!' do
- include_context 'fabrication context'
+ include_context 'with fabrication context'
it_behaves_like 'fabrication method', :fabricate_via_api!
@@ -77,18 +67,25 @@ RSpec.describe QA::Resource::Base do
expect(result).to eq(resource)
end
- it 'logs the resource and build method when QA_DEBUG=true' do
- stub_env('QA_DEBUG', 'true')
- expect(resource).to receive(:fabricate_via_api!).and_return(location)
+ context "with debug log level" do
+ before do
+ allow(QA::Runtime::Logger).to receive(:debug)
+ end
+
+ it 'logs the resource and build method' do
+ stub_env('QA_DEBUG', 'true')
+
+ subject.fabricate_via_api!('something', resource: resource, parents: [])
- expect { subject.fabricate_via_api!('something', resource: resource, parents: []) }
- .to output(/==> Built a MyResource via api in [\d\.\-e]+ seconds+/)
- .to_stdout
+ expect(QA::Runtime::Logger).to have_received(:debug) do |&msg|
+ expect(msg.call).to match_regex(/==> Built a MyResource via api in [\d.\-e]+ seconds+/)
+ end
+ end
end
end
describe '.fabricate_via_browser_ui!' do
- include_context 'fabrication context'
+ include_context 'with fabrication context'
it_behaves_like 'fabrication method', :fabricate_via_browser_ui!, :fabricate!
@@ -104,16 +101,24 @@ RSpec.describe QA::Resource::Base do
expect(result).to eq(resource)
end
- it 'logs the resource and build method when QA_DEBUG=true' do
- stub_env('QA_DEBUG', 'true')
+ context "with debug log level" do
+ before do
+ allow(QA::Runtime::Logger).to receive(:debug)
+ end
+
+ it 'logs the resource and build method' do
+ stub_env('QA_DEBUG', 'true')
+
+ subject.fabricate_via_browser_ui!('something', resource: resource, parents: [])
- expect { subject.fabricate_via_browser_ui!('something', resource: resource, parents: []) }
- .to output(/==> Built a MyResource via browser_ui in [\d\.\-e]+ seconds+/)
- .to_stdout
+ expect(QA::Runtime::Logger).to have_received(:debug) do |&msg|
+ expect(msg.call).to match_regex(/==> Built a MyResource via browser_ui in [\d.\-e]+ seconds+/)
+ end
+ end
end
end
- shared_context 'simple resource' do
+ shared_context 'with simple resource' do
subject do
Class.new(QA::Resource::Base) do
attribute :test do
@@ -136,7 +141,7 @@ RSpec.describe QA::Resource::Base do
end
describe '.attribute' do
- include_context 'simple resource'
+ include_context 'with simple resource'
context 'when the attribute is populated via a block' do
it 'returns value from the block' do
@@ -151,7 +156,7 @@ RSpec.describe QA::Resource::Base do
let(:api_resource) { { no_block: 'api' } }
before do
- expect(resource).to receive(:api_resource).and_return(api_resource)
+ allow(resource).to receive(:api_resource).and_return(api_resource)
end
it 'returns value from api' do
@@ -165,16 +170,16 @@ RSpec.describe QA::Resource::Base do
let(:api_resource) { { test: 'api_with_block' } }
before do
- allow(QA::Runtime::Logger).to receive(:info)
+ allow(QA::Runtime::Logger).to receive(:debug)
end
- it 'returns value from api and emits an INFO log entry' do
+ it 'returns value from api and emits an debug log entry' do
result = subject.fabricate!(resource: resource)
expect(result).to be_a(described_class)
expect(result.test).to eq('api_with_block')
expect(QA::Runtime::Logger)
- .to have_received(:info).with(/api_with_block/)
+ .to have_received(:debug).with(/api_with_block/)
end
end
end
@@ -209,8 +214,9 @@ RSpec.describe QA::Resource::Base do
it 'raises an error because no values could be found' do
result = subject.fabricate!(resource: resource)
- expect { result.no_block }
- .to raise_error(described_class::NoValueError, "No value was computed for no_block of #{resource.class.name}.")
+ expect { result.no_block }.to raise_error(
+ described_class::NoValueError, "No value was computed for no_block of #{resource.class.name}."
+ )
end
end
@@ -254,7 +260,7 @@ RSpec.describe QA::Resource::Base do
end
describe '#web_url' do
- include_context 'simple resource'
+ include_context 'with simple resource'
it 'sets #web_url to #current_url after fabrication' do
subject.fabricate!(resource: resource)
@@ -264,7 +270,7 @@ RSpec.describe QA::Resource::Base do
end
describe '#visit!' do
- include_context 'simple resource'
+ include_context 'with simple resource'
before do
allow(resource).to receive(:visit)
diff --git a/qa/spec/runtime/api/request_spec.rb b/qa/spec/runtime/api/request_spec.rb
index 93de2f4a87e..a1de71d31f0 100644
--- a/qa/spec/runtime/api/request_spec.rb
+++ b/qa/spec/runtime/api/request_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe QA::Runtime::API::Request do
end
context 'when oauth_access_token is passed in the query string' do
- let(:request) { described_class.new(client, '/users', { oauth_access_token: 'foo' }) }
+ let(:request) { described_class.new(client, '/users', oauth_access_token: 'foo') }
it 'does not adds a private_token query string' do
expect(request.url).to eq 'http://example.com/api/v4/users?oauth_access_token=foo'
diff --git a/qa/spec/spec_helper.rb b/qa/spec/spec_helper.rb
index f4bfd57504e..0df7b94b894 100644
--- a/qa/spec/spec_helper.rb
+++ b/qa/spec/spec_helper.rb
@@ -6,6 +6,9 @@ require 'rspec-parameterized'
require 'active_support/core_ext/hash'
require 'active_support/core_ext/object/blank'
+require_relative 'qa_deprecation_toolkit_env'
+QaDeprecationToolkitEnv.configure!
+
if ENV['CI'] && QA::Runtime::Env.knapsack? && !ENV['NO_KNAPSACK']
require 'knapsack'
Knapsack::Adapters::RSpecAdapter.bind
@@ -23,8 +26,8 @@ Dir[::File.join(__dir__, "support/shared_examples/*.rb")].sort.each { |f| requir
RSpec.configure do |config|
config.include ::Matchers
- QA::Specs::Helpers::Quarantine.configure_rspec
- QA::Specs::Helpers::ContextSelector.configure_rspec
+ config.add_formatter QA::Specs::Helpers::ContextFormatter
+ config.add_formatter QA::Specs::Helpers::QuarantineFormatter
config.before do |example|
QA::Runtime::Logger.debug("\nStarting test: #{example.full_description}\n")
diff --git a/qa/spec/specs/allure_report_spec.rb b/qa/spec/specs/allure_report_spec.rb
new file mode 100644
index 00000000000..27bc0dd3d1d
--- /dev/null
+++ b/qa/spec/specs/allure_report_spec.rb
@@ -0,0 +1,93 @@
+# frozen_string_literal: true
+
+require 'allure-rspec'
+
+describe QA::Runtime::AllureReport do
+ include Helpers::StubENV
+
+ let(:rspec_config) { double('RSpec::Core::Configuration', 'add_formatter': nil, after: nil) }
+
+ let(:png_path) { 'png_path' }
+ let(:html_path) { 'html_path' }
+
+ let(:allure_config) do
+ # need to mock config in case the test itself is executed with allure reporting enabled
+ AllureRspec::RspecConfig.send(:new).tap do |conf|
+ conf.instance_variable_set(:@allure_config, Allure::Config.send(:new))
+ end
+ end
+
+ before do
+ stub_env('QA_GENERATE_ALLURE_REPORT', generate_report)
+
+ allow(AllureRspec).to receive(:configure).and_yield(allure_config)
+ allow(RSpec).to receive(:configure).and_yield(rspec_config)
+ allow(Capybara::Screenshot).to receive(:after_save_screenshot).and_yield(png_path)
+ allow(Capybara::Screenshot).to receive(:after_save_html).and_yield(html_path)
+ end
+
+ context 'with report generation disabled' do
+ let(:generate_report) { 'false' }
+
+ it 'does not perform configuration' do
+ aggregate_failures do
+ expect(described_class.configure!).to be_nil
+
+ expect(AllureRspec).not_to have_received(:configure)
+ expect(RSpec).not_to have_received(:configure)
+ expect(Capybara::Screenshot).not_to have_received(:after_save_screenshot)
+ expect(Capybara::Screenshot).not_to have_received(:after_save_html)
+ end
+ end
+ end
+
+ context 'with report generation enabled' do
+ let(:generate_report) { 'true' }
+
+ let(:png_file) { 'png-file' }
+ let(:html_file) { 'html-file' }
+ let(:ci_job) { 'ee:relative 5' }
+
+ before do
+ stub_env('CI', 'true')
+ stub_env('CI_JOB_NAME', ci_job)
+
+ allow(Allure).to receive(:add_attachment)
+ allow(File).to receive(:open).with(png_path) { png_file }
+ allow(File).to receive(:open).with(html_path) { html_file }
+
+ described_class.configure!
+ end
+
+ it 'configures Allure options' do
+ aggregate_failures do
+ expect(allure_config.results_directory).to eq('tmp/allure-results')
+ expect(allure_config.clean_results_directory).to eq(true)
+ expect(allure_config.environment_properties).to be_a_kind_of(Hash)
+ expect(allure_config.environment).to eq('ee:relative')
+ end
+ end
+
+ it 'adds rspec and metadata formatter' do
+ expect(rspec_config).to have_received(:add_formatter).with(AllureRspecFormatter).ordered
+ expect(rspec_config).to have_received(:add_formatter).with(QA::Support::AllureMetadataFormatter).ordered
+ end
+
+ it 'configures screenshot saving' do
+ aggregate_failures do
+ expect(Allure).to have_received(:add_attachment).with(
+ name: 'screenshot',
+ source: png_file,
+ type: Allure::ContentType::PNG,
+ test_case: true
+ )
+ expect(Allure).to have_received(:add_attachment).with(
+ name: 'html',
+ source: html_file,
+ type: 'text/html',
+ test_case: true
+ )
+ end
+ end
+ end
+end
diff --git a/qa/spec/specs/helpers/context_selector_spec.rb b/qa/spec/specs/helpers/context_selector_spec.rb
index f0250103008..cbdbe6698ae 100644
--- a/qa/spec/specs/helpers/context_selector_spec.rb
+++ b/qa/spec/specs/helpers/context_selector_spec.rb
@@ -2,29 +2,25 @@
require 'rspec/core/sandbox'
-RSpec.configure do |c|
- c.around do |ex|
+RSpec.describe QA::Specs::Helpers::ContextSelector do
+ include Helpers::StubENV
+ include QA::Specs::Helpers::RSpec
+
+ around do |ex|
+ QA::Runtime::Scenario.define(:gitlab_address, 'https://staging.gitlab.com')
+
RSpec::Core::Sandbox.sandboxed do |config|
+ config.formatter = QA::Specs::Helpers::ContextFormatter
+
# If there is an example-within-an-example, we want to make sure the inner example
# does not get a reference to the outer example (the real spec) if it calls
# something like `pending`
config.before(:context) { RSpec.current_example = nil }
-
config.color_mode = :off
ex.run
end
end
-end
-
-RSpec.describe QA::Specs::Helpers::ContextSelector do
- include Helpers::StubENV
- include QA::Specs::Helpers::RSpec
-
- before do
- QA::Runtime::Scenario.define(:gitlab_address, 'https://staging.gitlab.com')
- described_class.configure_rspec
- end
describe '.context_matches?' do
it 'returns true when url has .com' do
@@ -104,7 +100,6 @@ RSpec.describe QA::Specs::Helpers::ContextSelector do
context 'with different environment set' do
before do
QA::Runtime::Scenario.define(:gitlab_address, 'https://gitlab.com')
- described_class.configure_rspec
end
it 'does not run against production' do
@@ -239,7 +234,6 @@ RSpec.describe QA::Specs::Helpers::ContextSelector do
context 'without CI_PROJECT_NAME set' do
before do
stub_env('CI_PROJECT_NAME', nil)
- described_class.configure_rspec
end
it 'runs on any pipeline' do
@@ -273,7 +267,6 @@ RSpec.describe QA::Specs::Helpers::ContextSelector do
context 'when a pipeline triggered from the default branch runs in gitlab-qa' do
before do
stub_env('CI_PROJECT_NAME', 'gitlab-qa')
- described_class.configure_rspec
end
it 'runs on default branch pipelines' do
@@ -310,7 +303,6 @@ RSpec.describe QA::Specs::Helpers::ContextSelector do
context 'with CI_PROJECT_NAME set' do
before do
stub_env('CI_PROJECT_NAME', 'NIGHTLY')
- described_class.configure_rspec
end
it 'runs on designated pipeline' do
@@ -353,7 +345,6 @@ RSpec.describe QA::Specs::Helpers::ContextSelector do
context 'without CI_JOB_NAME set' do
before do
stub_env('CI_JOB_NAME', nil)
- described_class.configure_rspec
end
context 'when excluding contexts' do
@@ -396,7 +387,6 @@ RSpec.describe QA::Specs::Helpers::ContextSelector do
context 'with CI_JOB_NAME set' do
before do
stub_env('CI_JOB_NAME', 'ee:instance-image')
- described_class.configure_rspec
end
context 'when excluding contexts' do
diff --git a/qa/spec/specs/helpers/quarantine_spec.rb b/qa/spec/specs/helpers/quarantine_spec.rb
index 45754a09b17..548a8510988 100644
--- a/qa/spec/specs/helpers/quarantine_spec.rb
+++ b/qa/spec/specs/helpers/quarantine_spec.rb
@@ -2,9 +2,14 @@
require 'rspec/core/sandbox'
-RSpec.configure do |c|
- c.around do |ex|
+RSpec.describe QA::Specs::Helpers::Quarantine do
+ include Helpers::StubENV
+ include QA::Specs::Helpers::RSpec
+
+ around do |ex|
RSpec::Core::Sandbox.sandboxed do |config|
+ config.formatter = QA::Specs::Helpers::QuarantineFormatter
+
# If there is an example-within-an-example, we want to make sure the inner example
# does not get a reference to the outer example (the real spec) if it calls
# something like `pending`
@@ -15,18 +20,9 @@ RSpec.configure do |c|
ex.run
end
end
-end
-
-RSpec.describe QA::Specs::Helpers::Quarantine do
- include Helpers::StubENV
- include QA::Specs::Helpers::RSpec
describe '.skip_or_run_quarantined_contexts' do
context 'with no tag focused' do
- before do
- described_class.configure_rspec
- end
-
it 'skips before hooks of quarantined contexts' do
executed_hooks = []
@@ -66,7 +62,6 @@ RSpec.describe QA::Specs::Helpers::Quarantine do
context 'with :quarantine focused' do
before do
- described_class.configure_rspec
RSpec.configure do |c|
c.filter_run :quarantine
end
@@ -110,10 +105,6 @@ RSpec.describe QA::Specs::Helpers::Quarantine do
describe '.skip_or_run_quarantined_tests_or_contexts' do
context 'with no tag focused' do
- before do
- described_class.configure_rspec
- end
-
it 'skips quarantined tests' do
group = describe_successfully do
it('is pending', :quarantine) {}
@@ -135,7 +126,6 @@ RSpec.describe QA::Specs::Helpers::Quarantine do
context 'with environment set' do
before do
QA::Runtime::Scenario.define(:gitlab_address, 'https://staging.gitlab.com')
- described_class.configure_rspec
end
context 'no pipeline specified' do
@@ -168,7 +158,6 @@ RSpec.describe QA::Specs::Helpers::Quarantine do
shared_examples 'skipped in project' do |project|
before do
stub_env('CI_PROJECT_NAME', project)
- described_class.configure_rspec
end
it "is skipped in #{project}" do
@@ -209,7 +198,6 @@ RSpec.describe QA::Specs::Helpers::Quarantine do
context 'with :quarantine focused' do
before do
- described_class.configure_rspec
RSpec.configure do |c|
c.filter_run :quarantine
end
@@ -234,7 +222,6 @@ RSpec.describe QA::Specs::Helpers::Quarantine do
context 'with a non-quarantine tag focused' do
before do
- described_class.configure_rspec
RSpec.configure do |c|
c.filter_run :foo
end
@@ -277,7 +264,6 @@ RSpec.describe QA::Specs::Helpers::Quarantine do
context 'with :quarantine and non-quarantine tags focused' do
before do
- described_class.configure_rspec
RSpec.configure do |c|
c.filter_run :foo, :bar, :quarantine
end
diff --git a/qa/spec/support/allure_metadata_formatter_spec.rb b/qa/spec/support/allure_metadata_formatter_spec.rb
new file mode 100644
index 00000000000..cb208642716
--- /dev/null
+++ b/qa/spec/support/allure_metadata_formatter_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+describe QA::Support::AllureMetadataFormatter do
+ include Helpers::StubENV
+
+ let(:formatter) { described_class.new(StringIO.new) }
+
+ let(:rspec_example_notification) { double('RSpec::Core::Notifications::ExampleNotification', example: rspec_example) }
+ let(:rspec_example) do
+ double(
+ 'RSpec::Core::Example',
+ tms: nil,
+ issue: nil,
+ add_link: nil,
+ attempts: 0,
+ file_path: 'file/path/spec.rb',
+ metadata: {
+ testcase: 'testcase',
+ quarantine: { issue: 'issue' }
+ }
+ )
+ end
+
+ let(:ci_job) { 'ee:relative 5' }
+ let(:ci_job_url) { 'url' }
+
+ before do
+ stub_env('CI', 'true')
+ stub_env('CI_JOB_NAME', ci_job)
+ stub_env('CI_JOB_URL', ci_job_url)
+ end
+
+ it "adds additional data to report" do
+ formatter.example_started(rspec_example_notification)
+
+ aggregate_failures do
+ expect(rspec_example).to have_received(:tms).with('Testcase', 'testcase')
+ expect(rspec_example).to have_received(:issue).with('Quarantine issue', 'issue')
+ expect(rspec_example).to have_received(:add_link).with(name: "Job(#{ci_job})", url: ci_job_url)
+ expect(rspec_example).to have_received(:issue).with(
+ 'Failure issues',
+ 'https://gitlab.com/gitlab-org/gitlab/-/issues?scope=all&state=opened&search=spec.rb'
+ )
+ end
+ end
+end
diff --git a/qa/spec/support/matchers/eventually_matcher.rb b/qa/spec/support/matchers/eventually_matcher.rb
index 3f0afd6fb54..7a35a3165ae 100644
--- a/qa/spec/support/matchers/eventually_matcher.rb
+++ b/qa/spec/support/matchers/eventually_matcher.rb
@@ -9,7 +9,7 @@
# expect { Something.that.takes.time.to_appear }.not_to eventually_eq(expected_result)
#
# With duration and attempts override
-# expect { Something.that.takes.time.to_appear }.to eventually_eq(expected_result).within(duration: 10, attempts: 5)
+# expect { Something.that.takes.time.to_appear }.to eventually_eq(expected_result).within(max_duration: 10, max_attempts: 5)
module Matchers
%w[
@@ -21,9 +21,9 @@ module Matchers
be_empty
].each do |op|
RSpec::Matchers.define(:"eventually_#{op}") do |*expected|
- chain(:within) do |options = {}|
- @duration = options[:duration]
- @attempts = options[:attempts]
+ chain(:within) do |kwargs = {}|
+ @retry_args = kwargs
+ @retry_args[:sleep_interval] = 0.5 unless @retry_args[:sleep_interval]
end
def supports_block_expectations?
@@ -52,11 +52,12 @@ module Matchers
# @param [Symbol] expectation_name
# @return [Boolean]
def wait_and_check(actual, expectation_name)
- QA::Support::Retrier.retry_until(
- max_attempts: @attempts,
- max_duration: @duration,
- sleep_interval: 0.5
- ) do
+ attempt = 0
+
+ QA::Runtime::Logger.debug("Running eventually matcher with '#{operator_msg}' operator")
+ QA::Support::Retrier.retry_until(**@retry_args) do
+ QA::Runtime::Logger.debug("evaluating expectation, attempt: #{attempt += 1}")
+
public_send(expectation_name, actual)
rescue RSpec::Expectations::ExpectationNotMetError, QA::Resource::ApiFabricator::ResourceNotFoundError
false
diff --git a/qa/spec/support/retrier_spec.rb b/qa/spec/support/retrier_spec.rb
index 6f052519516..4e27915553c 100644
--- a/qa/spec/support/retrier_spec.rb
+++ b/qa/spec/support/retrier_spec.rb
@@ -70,8 +70,9 @@ RSpec.describe QA::Support::Retrier do
describe '.retry_on_exception' do
context 'when the condition is true' do
it 'logs max_attempts, reload_page, and sleep_interval parameters' do
- expect { subject.retry_on_exception(max_attempts: 1, reload_page: nil, sleep_interval: 0) { true } }
- .to output(/with retry_on_exception: max_attempts: 1; reload_page: ; sleep_interval: 0/).to_stdout_from_any_process
+ message = /with retry_on_exception: max_attempts: 1; reload_page: true; sleep_interval: 0/
+ expect { subject.retry_on_exception(max_attempts: 1, reload_page: true, sleep_interval: 0) { true } }
+ .to output(message).to_stdout_from_any_process
end
it 'logs the end' do
@@ -82,8 +83,9 @@ RSpec.describe QA::Support::Retrier do
context 'when the condition is false' do
it 'logs the start' do
- expect { subject.retry_on_exception(max_attempts: 1, reload_page: nil, sleep_interval: 0) { false } }
- .to output(/with retry_on_exception: max_attempts: 1; reload_page: ; sleep_interval: 0/).to_stdout_from_any_process
+ message = /with retry_on_exception: max_attempts: 1; reload_page: true; sleep_interval: 0/
+ expect { subject.retry_on_exception(max_attempts: 1, reload_page: true, sleep_interval: 0) { false } }
+ .to output(message).to_stdout_from_any_process
end
it 'logs the end' do