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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/qa
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-01-21 17:21:10 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-01-21 17:21:10 +0300
commitcb0d23c455b73486fd1015f8ca9479b5b7e3585d (patch)
treed7dc129a407fd74266d2dc561bebf24665197c2f /qa
parentc3e911be175c0aabfea1eb030f9e0ef23f5f3887 (diff)
Add latest changes from gitlab-org/gitlab@12-7-stable-ee
Diffstat (limited to 'qa')
-rw-r--r--qa/Dockerfile6
-rw-r--r--qa/Gemfile1
-rw-r--r--qa/Gemfile.lock2
-rw-r--r--qa/README.md2
-rw-r--r--qa/load/artillery.yml25
-rw-r--r--qa/qa.rb5
-rw-r--r--qa/qa/fixtures/auto_devops_rack/Dockerfile2
-rw-r--r--qa/qa/page/base.rb68
-rw-r--r--qa/qa/page/component/ci_badge_link.rb2
-rw-r--r--qa/qa/page/component/clone_panel.rb2
-rw-r--r--qa/qa/page/component/dropdown_filter.rb4
-rw-r--r--qa/qa/page/component/dropzone.rb2
-rw-r--r--qa/qa/page/component/groups_filter.rb4
-rw-r--r--qa/qa/page/component/issuable/common.rb5
-rw-r--r--qa/qa/page/component/legacy_clone_panel.rb2
-rw-r--r--qa/qa/page/component/note.rb12
-rw-r--r--qa/qa/page/file/shared/commit_button.rb2
-rw-r--r--qa/qa/page/group/settings/general.rb12
-rw-r--r--qa/qa/page/group/show.rb2
-rw-r--r--qa/qa/page/group/sub_menus/members.rb9
-rw-r--r--qa/qa/page/layout/performance_bar.rb18
-rw-r--r--qa/qa/page/main/menu.rb10
-rw-r--r--qa/qa/page/merge_request/show.rb58
-rw-r--r--qa/qa/page/project/branches/show.rb2
-rw-r--r--qa/qa/page/project/import/github.rb16
-rw-r--r--qa/qa/page/project/issue/index.rb11
-rw-r--r--qa/qa/page/project/issue/show.rb18
-rw-r--r--qa/qa/page/project/job/show.rb12
-rw-r--r--qa/qa/page/project/operations/environments/index.rb4
-rw-r--r--qa/qa/page/project/operations/kubernetes/index.rb4
-rw-r--r--qa/qa/page/project/operations/kubernetes/show.rb22
-rw-r--r--qa/qa/page/project/pipeline/index.rb2
-rw-r--r--qa/qa/page/project/pipeline/show.rb8
-rw-r--r--qa/qa/page/project/settings/ci_variables.rb8
-rw-r--r--qa/qa/page/project/settings/deploy_keys.rb20
-rw-r--r--qa/qa/page/project/settings/deploy_tokens.rb4
-rw-r--r--qa/qa/page/project/settings/members.rb12
-rw-r--r--qa/qa/page/project/settings/mirroring_repositories.rb10
-rw-r--r--qa/qa/page/project/settings/protected_branches.rb7
-rw-r--r--qa/qa/page/project/show.rb8
-rw-r--r--qa/qa/page/project/web_ide/edit.rb24
-rw-r--r--qa/qa/page/search/results.rb15
-rw-r--r--qa/qa/page/settings/common.rb2
-rw-r--r--qa/qa/page/validatable.rb3
-rw-r--r--qa/qa/resource/base.rb12
-rw-r--r--qa/qa/resource/deploy_key.rb4
-rw-r--r--qa/qa/resource/events/base.rb7
-rw-r--r--qa/qa/resource/group.rb2
-rw-r--r--qa/qa/resource/issue.rb4
-rw-r--r--qa/qa/resource/merge_request.rb2
-rw-r--r--qa/qa/resource/merge_request_from_fork.rb4
-rw-r--r--qa/qa/resource/project_imported_from_github.rb1
-rw-r--r--qa/qa/resource/protected_branch.rb9
-rw-r--r--qa/qa/resource/repository/commit.rb2
-rw-r--r--qa/qa/resource/repository/project_push.rb4
-rw-r--r--qa/qa/resource/repository/push.rb3
-rw-r--r--qa/qa/resource/runner.rb2
-rw-r--r--qa/qa/resource/sandbox.rb8
-rw-r--r--qa/qa/resource/ssh_key.rb2
-rw-r--r--qa/qa/runtime/application_settings.rb46
-rw-r--r--qa/qa/runtime/browser.rb33
-rw-r--r--qa/qa/runtime/env.rb4
-rw-r--r--qa/qa/runtime/feature.rb2
-rw-r--r--qa/qa/runtime/key/base.rb4
-rw-r--r--qa/qa/runtime/logger.rb8
-rw-r--r--qa/qa/runtime/search.rb79
-rw-r--r--qa/qa/scenario/template.rb5
-rw-r--r--qa/qa/scenario/test/instance.rb5
-rw-r--r--qa/qa/service/cluster_provider/k3d.rb3
-rw-r--r--qa/qa/service/kubernetes_cluster.rb4
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/login/login_via_oauth_spec.rb3
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/email/trigger_email_notification_spec.rb15
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/issue/check_mentions_for_xss_spec.rb14
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/issue/close_issue_spec.rb9
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/issue/collapse_comments_in_discussions_spec.rb14
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/issue/comment_issue_spec.rb5
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb12
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb8
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/issue/mentions_spec.rb12
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_diff_patch_spec.rb9
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb4
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb4
-rw-r--r--qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb7
-rw-r--r--qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb3
-rw-r--r--qa/qa/specs/features/browser_ui/7_configure/kubernetes/kubernetes_integration_spec.rb53
-rw-r--r--qa/qa/specs/features/browser_ui/non_devops/performance_bar_spec.rb46
-rw-r--r--qa/qa/specs/helpers/quarantine.rb18
-rw-r--r--qa/qa/support/page/logging.rb7
-rw-r--r--qa/qa/support/repeater.rb65
-rw-r--r--qa/qa/support/retrier.rb74
-rw-r--r--qa/qa/support/wait_for_requests.rb25
-rw-r--r--qa/qa/support/waiter.rb37
-rw-r--r--qa/qa/vendor/github/page/login.rb13
-rw-r--r--qa/qa/vendor/jenkins/page/configure.rb2
-rw-r--r--qa/qa/vendor/jenkins/page/login.rb2
-rw-r--r--qa/qa/vendor/jenkins/page/new_credentials.rb2
-rw-r--r--qa/qa/vendor/one_password/cli.rb35
-rwxr-xr-xqa/qa/vendor/one_password/darwin/opbin10616336 -> 11185296 bytes
-rwxr-xr-xqa/qa/vendor/one_password/linux/opbin9003392 -> 7712832 bytes
-rw-r--r--qa/spec/page/base_spec.rb28
-rw-r--r--qa/spec/page/logging_spec.rb32
-rw-r--r--qa/spec/resource/events/project_spec.rb1
-rw-r--r--qa/spec/runtime/application_settings_spec.rb43
-rw-r--r--qa/spec/runtime/env_spec.rb14
-rw-r--r--qa/spec/spec_helper.rb6
-rw-r--r--qa/spec/specs/helpers/quarantine_spec.rb20
-rw-r--r--qa/spec/support/helpers/stub_env.rb (renamed from qa/spec/helpers/stub_env.rb)0
-rw-r--r--qa/spec/support/repeater_spec.rb385
-rw-r--r--qa/spec/support/retrier_spec.rb126
-rw-r--r--qa/spec/support/shared_examples/scenario_shared_examples.rb (renamed from qa/spec/shared_examples/scenario_shared_examples.rb)6
-rw-r--r--qa/spec/support/waiter_spec.rb42
113 files changed, 1458 insertions, 473 deletions
diff --git a/qa/Dockerfile b/qa/Dockerfile
index e4b860b08b2..126d9fbc591 100644
--- a/qa/Dockerfile
+++ b/qa/Dockerfile
@@ -39,6 +39,12 @@ RUN wget -q https://chromedriver.storage.googleapis.com/$(wget -q -O - https://c
RUN unzip chromedriver_linux64.zip -d /usr/local/bin
##
+# Install K3d local cluster support
+# https://github.com/rancher/k3d
+#
+RUN curl -s https://raw.githubusercontent.com/rancher/k3d/master/install.sh | TAG=v1.3.4 bash
+
+##
# Install gcloud and kubectl CLI used in Auto DevOps test to create K8s
# clusters
#
diff --git a/qa/Gemfile b/qa/Gemfile
index 3575ecf13e9..58118340f24 100644
--- a/qa/Gemfile
+++ b/qa/Gemfile
@@ -19,4 +19,5 @@ group :test do
gem 'pry-byebug', '~> 3.5.1', platform: :mri
gem "ruby-debug-ide", "~> 0.7.0"
gem "debase", "~> 0.2.4.1"
+ gem 'timecop', '~> 0.9.1'
end
diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock
index 25c7703ef52..6d48a9449a5 100644
--- a/qa/Gemfile.lock
+++ b/qa/Gemfile.lock
@@ -99,6 +99,7 @@ GEM
childprocess (>= 0.5, < 4.0)
rubyzip (>= 1.2.2)
thread_safe (0.3.6)
+ timecop (0.9.1)
tzinfo (1.2.5)
thread_safe (~> 0.1)
unf (0.1.4)
@@ -128,6 +129,7 @@ DEPENDENCIES
rspec_junit_formatter (~> 0.4.1)
ruby-debug-ide (~> 0.7.0)
selenium-webdriver (~> 3.12)
+ timecop (~> 0.9.1)
BUNDLED WITH
1.17.3
diff --git a/qa/README.md b/qa/README.md
index 332e5c8170f..1bfa83cadf1 100644
--- a/qa/README.md
+++ b/qa/README.md
@@ -30,7 +30,7 @@ and corresponding views / partials / selectors in CE / EE.
Whenever `qa:selectors` job fails in your merge request, you are supposed to
fix [page objects](../doc/development/testing_guide/end_to_end/page_objects.md). You should also trigger end-to-end tests
-using `package-and-qa-manual` manual action, to test if everything works fine.
+using `package-and-qa` manual action, to test if everything works fine.
## How can I use it?
diff --git a/qa/load/artillery.yml b/qa/load/artillery.yml
deleted file mode 100644
index 17d253ec480..00000000000
--- a/qa/load/artillery.yml
+++ /dev/null
@@ -1,25 +0,0 @@
-config:
- target: "{{ $processEnvironment.HOST_URL }}"
- http:
- pool: 10 # All HTTP requests from all virtual users will be sent over the same <pool> connections.
- # This also means that there is a limit on the number of requests sent per second.
- phases:
- - duration: 30
- arrivalRate: 10
- name: "Warm up"
- - duration: 90
- arrivalRate: 10
- rampTo: 100
- name: "Gradual ramp up"
- - duration: 90
- arrivalRate: 100
- name: "Sustained max load"
-scenarios:
- - name: "Visit large issue url"
- flow:
- - get:
- url: "{{ $processEnvironment.LARGE_ISSUE_URL }}"
- - name: "Visit large MR url"
- flow:
- - get:
- url: "{{ $processEnvironment.LARGE_MR_URL }}"
diff --git a/qa/qa.rb b/qa/qa.rb
index 509de4af79c..a0ce6caa3a9 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -37,6 +37,7 @@ module QA
autoload :MailHog, 'qa/runtime/mail_hog'
autoload :IPAddress, 'qa/runtime/ip_address'
autoload :Search, 'qa/runtime/search'
+ autoload :ApplicationSettings, 'qa/runtime/application_settings'
module API
autoload :Client, 'qa/runtime/api/client'
@@ -487,8 +488,10 @@ module QA
end
autoload :Api, 'qa/support/api'
autoload :Dates, 'qa/support/dates'
- autoload :Waiter, 'qa/support/waiter'
+ autoload :Repeater, 'qa/support/repeater'
autoload :Retrier, 'qa/support/retrier'
+ autoload :Waiter, 'qa/support/waiter'
+ autoload :WaitForRequests, 'qa/support/wait_for_requests'
end
end
diff --git a/qa/qa/fixtures/auto_devops_rack/Dockerfile b/qa/qa/fixtures/auto_devops_rack/Dockerfile
index 1f59c23ea88..6ab2795dd40 100644
--- a/qa/qa/fixtures/auto_devops_rack/Dockerfile
+++ b/qa/qa/fixtures/auto_devops_rack/Dockerfile
@@ -1,4 +1,4 @@
-FROM ruby:2.6.3-alpine
+FROM ruby:2.6.5-alpine
ADD ./ /app/
WORKDIR /app
ENV RACK_ENV production
diff --git a/qa/qa/page/base.rb b/qa/qa/page/base.rb
index dcba4fc8544..a4c44f78ad4 100644
--- a/qa/qa/page/base.rb
+++ b/qa/qa/page/base.rb
@@ -8,6 +8,7 @@ module QA
prepend Support::Page::Logging if Runtime::Env.debug?
include Capybara::DSL
include Scenario::Actable
+ include Support::WaitForRequests
extend Validatable
extend SingleForwardable
@@ -21,27 +22,31 @@ module QA
def refresh
page.refresh
+
+ wait_for_requests
end
- def wait(max: 60, interval: 0.1, reload: true)
- QA::Support::Waiter.wait(max: max, interval: interval) do
+ def wait_until(max_duration: 60, sleep_interval: 0.1, reload: true, raise_on_failure: false)
+ Support::Waiter.wait_until(max_duration: max_duration, sleep_interval: sleep_interval, raise_on_failure: raise_on_failure) do
yield || (reload && refresh && false)
end
end
- def retry_until(max_attempts: 3, reload: false, sleep_interval: 0)
- QA::Support::Retrier.retry_until(max_attempts: max_attempts, reload_page: (reload && self), sleep_interval: sleep_interval) do
+ def retry_until(max_attempts: 3, reload: false, sleep_interval: 0, raise_on_failure: false)
+ Support::Retrier.retry_until(max_attempts: max_attempts, reload_page: (reload && self), sleep_interval: sleep_interval, raise_on_failure: raise_on_failure) do
yield
end
end
def retry_on_exception(max_attempts: 3, reload: false, sleep_interval: 0.5)
- QA::Support::Retrier.retry_on_exception(max_attempts: max_attempts, reload_page: (reload && self), sleep_interval: sleep_interval) do
+ Support::Retrier.retry_on_exception(max_attempts: max_attempts, reload_page: (reload && self), sleep_interval: sleep_interval) do
yield
end
end
def scroll_to(selector, text: nil)
+ wait_for_requests
+
page.execute_script <<~JS
var elements = Array.from(document.querySelectorAll('#{selector}'));
var text = '#{text}';
@@ -66,7 +71,7 @@ module QA
xhr.send();
JS
- return false unless wait(interval: 0.5, max: 60, reload: false) do
+ return false unless wait_until(sleep_interval: 0.5, max_duration: 60, reload: false) do
page.evaluate_script('xhr.readyState == XMLHttpRequest.DONE')
end
@@ -74,6 +79,8 @@ module QA
end
def find_element(name, **kwargs)
+ wait_for_requests
+
find(element_selector_css(name), kwargs)
end
@@ -82,6 +89,12 @@ module QA
end
def all_elements(name, **kwargs)
+ if kwargs.keys.none? { |key| [:minimum, :maximum, :count, :between].include?(key) }
+ raise ArgumentError, "Please use :minimum, :maximum, :count, or :between so that all is more reliable"
+ end
+
+ wait_for_requests
+
all(element_selector_css(name), **kwargs)
end
@@ -102,8 +115,8 @@ module QA
end
# replace with (..., page = self.class)
- def click_element(name, page = nil, text: nil)
- find_element(name, text: text).click
+ def click_element(name, page = nil, text: nil, wait: Capybara.default_max_wait_time)
+ find_element(name, text: text, wait: wait).click
page.validate_elements_present! if page
end
@@ -119,33 +132,48 @@ module QA
element.select value
end
+ def has_active_element?(name, **kwargs)
+ has_element?(name, class: 'active', **kwargs)
+ end
+
def has_element?(name, **kwargs)
- wait = kwargs[:wait] ? kwargs[:wait] && kwargs.delete(:wait) : Capybara.default_max_wait_time
- text = kwargs[:text] ? kwargs[:text] && kwargs.delete(:text) : nil
+ wait_for_requests
- has_css?(element_selector_css(name, kwargs), text: text, wait: wait)
+ wait = kwargs.delete(:wait) || Capybara.default_max_wait_time
+ text = kwargs.delete(:text)
+ klass = kwargs.delete(:class)
+
+ has_css?(element_selector_css(name, kwargs), text: text, wait: wait, class: klass)
end
def has_no_element?(name, **kwargs)
- wait = kwargs[:wait] ? kwargs[:wait] && kwargs.delete(:wait) : Capybara.default_max_wait_time
- text = kwargs[:text] ? kwargs[:text] && kwargs.delete(:text) : nil
+ wait_for_requests
+
+ wait = kwargs.delete(:wait) || Capybara.default_max_wait_time
+ text = kwargs.delete(:text)
has_no_css?(element_selector_css(name, kwargs), wait: wait, text: text)
end
def has_text?(text, wait: Capybara.default_max_wait_time)
+ wait_for_requests
+
page.has_text?(text, wait: wait)
end
- def has_no_text?(text)
- page.has_no_text? text
+ def has_no_text?(text, wait: Capybara.default_max_wait_time)
+ wait_for_requests
+
+ page.has_no_text?(text, wait: wait)
end
def has_normalized_ws_text?(text, wait: Capybara.default_max_wait_time)
- page.has_text?(text.gsub(/\s+/, " "), wait: wait)
+ has_text?(text.gsub(/\s+/, " "), wait: wait)
end
def finished_loading?
+ wait_for_requests
+
# The number of selectors should be able to be reduced after
# migration to the new spinner is complete.
# https://gitlab.com/groups/gitlab-org/-/epics/956
@@ -153,6 +181,8 @@ module QA
end
def finished_loading_block?
+ wait_for_requests
+
has_no_css?('.fa-spinner.block-loading', wait: Capybara.default_max_wait_time)
end
@@ -161,7 +191,7 @@ module QA
# This loop gives time for the img tags to be rendered and for
# images to start loading.
previous_total_images = 0
- wait(interval: 1) do
+ wait_until(sleep_interval: 1) do
current_total_images = all("img").size
result = previous_total_images == current_total_images
previous_total_images = current_total_images
@@ -220,10 +250,14 @@ module QA
end
def click_link_with_text(text)
+ wait_for_requests
+
click_link text
end
def click_body
+ wait_for_requests
+
find('body').click
end
diff --git a/qa/qa/page/component/ci_badge_link.rb b/qa/qa/page/component/ci_badge_link.rb
index aad8dc1d3df..d3e44fd867d 100644
--- a/qa/qa/page/component/ci_badge_link.rb
+++ b/qa/qa/page/component/ci_badge_link.rb
@@ -26,7 +26,7 @@ module QA
private
def completed?(timeout: 60)
- wait(reload: false, max: timeout) do
+ wait_until(reload: false, max_duration: timeout) do
COMPLETED_STATUSES.include?(status_badge)
end
end
diff --git a/qa/qa/page/component/clone_panel.rb b/qa/qa/page/component/clone_panel.rb
index b80877f5ecd..fbe19e5802b 100644
--- a/qa/qa/page/component/clone_panel.rb
+++ b/qa/qa/page/component/clone_panel.rb
@@ -24,7 +24,7 @@ module QA
private
def repository_clone_location(kind)
- wait(reload: false) do
+ wait_until(reload: false) do
click_element :clone_dropdown
within_element :clone_options do
diff --git a/qa/qa/page/component/dropdown_filter.rb b/qa/qa/page/component/dropdown_filter.rb
index e896c382779..a39a04a668d 100644
--- a/qa/qa/page/component/dropdown_filter.rb
+++ b/qa/qa/page/component/dropdown_filter.rb
@@ -5,9 +5,7 @@ module QA
module Component
module DropdownFilter
def filter_and_select(item)
- wait(reload: false) do
- page.has_css?('.dropdown-input-field')
- end
+ page.has_css?('.dropdown-input-field', wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME)
find('.dropdown-input-field').set(item)
click_link item
diff --git a/qa/qa/page/component/dropzone.rb b/qa/qa/page/component/dropzone.rb
index 757111f240b..2efb96a02bc 100644
--- a/qa/qa/page/component/dropzone.rb
+++ b/qa/qa/page/component/dropzone.rb
@@ -23,7 +23,7 @@ module QA
page.attach_file(attachment, class: 'dz-hidden-input', make_visible: field_style)
# Wait for link to be appended to dropzone text
- page.wait(reload: false) do
+ page.wait_until(reload: false) do
page.find("#{container} textarea").value.match(filename)
end
end
diff --git a/qa/qa/page/component/groups_filter.rb b/qa/qa/page/component/groups_filter.rb
index cc50bb439b4..7eb1257db71 100644
--- a/qa/qa/page/component/groups_filter.rb
+++ b/qa/qa/page/component/groups_filter.rb
@@ -23,9 +23,7 @@ module QA
# Since we submitted after filtering, the presence of
# groups_list_tree_container means we have the complete filtered list
# of groups
- wait(reload: false) do
- page.has_css?(element_selector_css(:groups_list_tree_container))
- end
+ has_element?(:groups_list_tree_container, wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME)
# If there are no groups we'll know immediately because we filtered the list
return false if page.has_text?('No groups or projects matched your search', wait: 0)
diff --git a/qa/qa/page/component/issuable/common.rb b/qa/qa/page/component/issuable/common.rb
index 9ecc8f73bdb..1155d4da036 100644
--- a/qa/qa/page/component/issuable/common.rb
+++ b/qa/qa/page/component/issuable/common.rb
@@ -23,11 +23,6 @@ module QA
element :save_button
element :delete_button
end
-
- base.view 'app/assets/javascripts/issue_show/components/edit_actions.vue' do
- element :save_button
- element :delete_button
- end
end
end
end
diff --git a/qa/qa/page/component/legacy_clone_panel.rb b/qa/qa/page/component/legacy_clone_panel.rb
index e495cf4ef04..7b4b30623a6 100644
--- a/qa/qa/page/component/legacy_clone_panel.rb
+++ b/qa/qa/page/component/legacy_clone_panel.rb
@@ -30,7 +30,7 @@ module QA
private
def choose_repository_clone(kind, detect_text)
- wait(reload: false) do
+ wait_until(reload: false) do
click_element :clone_dropdown
page.within('.clone-options-dropdown') do
diff --git a/qa/qa/page/component/note.rb b/qa/qa/page/component/note.rb
index c85fa690d6c..3e8ed9069ce 100644
--- a/qa/qa/page/component/note.rb
+++ b/qa/qa/page/component/note.rb
@@ -45,17 +45,17 @@ module QA
click_element :comment_button
end
- def toggle_comments
- all_elements(:toggle_comments_button).last.click
+ def toggle_comments(position)
+ all_elements(:toggle_comments_button, minimum: position)[position - 1].click
end
- def type_reply_to_discussion(reply_text)
- all_elements(:discussion_reply_tab).last.click
+ def type_reply_to_discussion(position, reply_text)
+ all_elements(:discussion_reply_tab, minimum: position)[position - 1].click
fill_element :reply_input, reply_text
end
- def reply_to_discussion(reply_text)
- type_reply_to_discussion(reply_text)
+ def reply_to_discussion(position, reply_text)
+ type_reply_to_discussion(position, reply_text)
click_element :reply_comment_button
end
diff --git a/qa/qa/page/file/shared/commit_button.rb b/qa/qa/page/file/shared/commit_button.rb
index 559b4c6ceea..9ea4f4e7818 100644
--- a/qa/qa/page/file/shared/commit_button.rb
+++ b/qa/qa/page/file/shared/commit_button.rb
@@ -14,7 +14,7 @@ module QA
def commit_changes
click_element(:commit_button)
- wait(reload: false, max: 60) do
+ wait_until(reload: false, max_duration: 60) do
finished_loading?
end
end
diff --git a/qa/qa/page/group/settings/general.rb b/qa/qa/page/group/settings/general.rb
index efc8bbd7482..4a30403fda8 100644
--- a/qa/qa/page/group/settings/general.rb
+++ b/qa/qa/page/group/settings/general.rb
@@ -94,6 +94,18 @@ module QA
select_element(:project_creation_level_dropdown, value)
click_element :save_permissions_changes_button
end
+
+ def toggle_request_access
+ expand_section :permission_lfs_2fa_section
+
+ if find_element(:request_access_checkbox).checked?
+ uncheck_element :request_access_checkbox
+ else
+ check_element :request_access_checkbox
+ end
+
+ click_element :save_permissions_changes_button
+ end
end
end
end
diff --git a/qa/qa/page/group/show.rb b/qa/qa/page/group/show.rb
index e1f319da134..7639def98b7 100644
--- a/qa/qa/page/group/show.rb
+++ b/qa/qa/page/group/show.rb
@@ -58,7 +58,7 @@ module QA
QA::Support::Retrier.retry_on_exception(sleep_interval: 1.0) do
within_element(:new_project_or_subgroup_dropdown) do
# May need to click again because it is possible to click the button quicker than the JS is bound
- wait(reload: false) do
+ wait_until(reload: false) do
click_element :new_project_or_subgroup_dropdown_toggle
has_element?(kind)
diff --git a/qa/qa/page/group/sub_menus/members.rb b/qa/qa/page/group/sub_menus/members.rb
index c8b3f5bb422..33c4caaddcb 100644
--- a/qa/qa/page/group/sub_menus/members.rb
+++ b/qa/qa/page/group/sub_menus/members.rb
@@ -7,12 +7,9 @@ module QA
class Members < Page::Base
include Page::Component::UsersSelect
- view 'app/views/groups/group_members/_new_group_member.html.haml' do
- element :add_to_group_button
- end
-
- view 'app/helpers/groups/group_members_helper.rb' do
+ view 'app/views/shared/members/_invite_member.html.haml' do
element :member_select_field
+ element :invite_member_button
end
view 'app/views/shared/members/_member.html.haml' do
@@ -24,7 +21,7 @@ module QA
def add_member(username)
select_user :member_select_field, username
- click_element :add_to_group_button
+ click_element :invite_member_button
end
def update_access_level(username, access_level)
diff --git a/qa/qa/page/layout/performance_bar.rb b/qa/qa/page/layout/performance_bar.rb
index 79e4d3edce0..4e144e67f12 100644
--- a/qa/qa/page/layout/performance_bar.rb
+++ b/qa/qa/page/layout/performance_bar.rb
@@ -9,25 +9,31 @@ module QA
end
view 'app/assets/javascripts/performance_bar/components/detailed_metric.vue' do
- element :performance_bar_detailed_metric
+ element :detailed_metric_content
end
view 'app/assets/javascripts/performance_bar/components/request_selector.vue' do
- element :performance_bar_request
+ element :request_dropdown_option
+ element :request_dropdown
end
def has_performance_bar?
has_element?(:performance_bar)
end
- def has_detailed_metrics?
- all_elements(:performance_bar_detailed_metric).all? do |metric|
- metric.has_text?(%r{\d+})
+ def has_detailed_metrics?(count)
+ retry_until(sleep_interval: 1) do
+ all_elements(:detailed_metric_content, count: count).all? do |metric|
+ metric.has_text?(%r{\d+})
+ end
end
end
def has_request_for?(path)
- has_element?(:performance_bar_request, text: path)
+ click_element(:request_dropdown)
+ retry_until(sleep_interval: 1) do
+ has_element?(:request_dropdown_option, text: path)
+ end
end
end
end
diff --git a/qa/qa/page/main/menu.rb b/qa/qa/page/main/menu.rb
index 5f4b3946e6a..8ad30632fa1 100644
--- a/qa/qa/page/main/menu.rb
+++ b/qa/qa/page/main/menu.rb
@@ -76,8 +76,14 @@ module QA
end
def sign_out
- within_user_menu do
- click_element :sign_out_link
+ retry_until do
+ break true unless signed_in?
+
+ within_user_menu do
+ click_element :sign_out_link
+ end
+
+ has_no_element?(:user_avatar)
end
end
diff --git a/qa/qa/page/merge_request/show.rb b/qa/qa/page/merge_request/show.rb
index c3645a6a755..ad5b3c97cb9 100644
--- a/qa/qa/page/merge_request/show.rb
+++ b/qa/qa/page/merge_request/show.rb
@@ -6,6 +6,10 @@ module QA
class Show < Page::Base
include Page::Component::Note
+ view 'app/assets/javascripts/mr_tabs_popover/components/popover.vue' do
+ element :dismiss_popover_button
+ end
+
view 'app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue' do
element :dropdown_toggle
element :download_email_patches
@@ -29,6 +33,10 @@ module QA
element :merged_status_content
end
+ view 'app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue' do
+ element :merge_request_error_content
+ end
+
view 'app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue' do
element :mr_rebase_button
element :no_fast_forward_message, 'Fast-forward merge is not possible' # rubocop:disable QA/ElementWithPattern
@@ -38,6 +46,10 @@ module QA
element :squash_checkbox
end
+ view 'app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue' do
+ element :skeleton_note
+ end
+
view 'app/views/projects/merge_requests/show.html.haml' do
element :notes_tab
element :diffs_tab
@@ -61,32 +73,34 @@ module QA
end
def add_comment_to_diff(text)
- wait(interval: 5) do
+ wait_until(sleep_interval: 5) do
has_text?("No newline at end of file")
end
- all_elements(:new_diff_line).first.hover
- click_element :diff_comment
- fill_element :reply_input, text
+ all_elements(:new_diff_line, minimum: 1).first.hover
+ click_element(:diff_comment)
+ fill_element(:reply_input, text)
end
def click_discussions_tab
- click_element :notes_tab
+ click_element(:notes_tab)
- finished_loading?
+ wait_for_loading
end
def click_diffs_tab
- click_element :diffs_tab
+ click_element(:diffs_tab)
- finished_loading?
+ wait_for_loading
+
+ click_element(:dismiss_popover_button) if has_element?(:dismiss_popover_button)
end
def click_pipeline_link
- click_element :pipeline_link
+ click_element(:pipeline_link)
end
def edit!
- click_element :edit_button
+ click_element(:edit_button)
end
def fast_forward_possible?
@@ -126,12 +140,12 @@ module QA
def mark_to_squash
# The squash checkbox is disabled on load
- wait do
+ wait_until do
has_element?(:squash_checkbox)
end
# The squash checkbox is enabled via JS
- wait(reload: false) do
+ wait_until(reload: false) do
!find_element(:squash_checkbox).disabled?
end
@@ -150,30 +164,30 @@ module QA
def ready_to_merge?
# The merge button is disabled on load
- wait do
+ wait_until do
has_element?(:merge_button)
end
# The merge button is enabled via JS
- wait(reload: false) do
+ wait_until(reload: false) do
!find_element(:merge_button).disabled?
end
end
def rebase!
# The rebase button is disabled on load
- wait do
+ wait_until do
has_element?(:mr_rebase_button)
end
# The rebase button is enabled via JS
- wait(reload: false) do
+ wait_until(reload: false) do
!find_element(:mr_rebase_button).disabled?
end
click_element :mr_rebase_button
- success = wait do
+ success = wait_until do
has_text?('Fast-forward merge without a merge commit')
end
@@ -193,6 +207,16 @@ module QA
click_element :dropdown_toggle
visit_link_in_element(:download_plain_diff)
end
+
+ def wait_for_merge_request_error_message
+ wait_until(max_duration: 30, reload: false) do
+ has_element?(:merge_request_error_content)
+ end
+ end
+
+ def wait_for_loading
+ finished_loading? && has_no_element?(:skeleton_note)
+ end
end
end
end
diff --git a/qa/qa/page/project/branches/show.rb b/qa/qa/page/project/branches/show.rb
index 480fc7d78cb..63021df30f6 100644
--- a/qa/qa/page/project/branches/show.rb
+++ b/qa/qa/page/project/branches/show.rb
@@ -29,7 +29,7 @@ module QA
end
def has_no_branch?(branch_name, reload: false)
- wait(reload: reload) do
+ wait_until(reload: reload) do
within_element(:all_branches) do
has_no_element?(:branch_name, text: branch_name)
end
diff --git a/qa/qa/page/project/import/github.rb b/qa/qa/page/project/import/github.rb
index cc0c4e1e835..b533e0096a8 100644
--- a/qa/qa/page/project/import/github.rb
+++ b/qa/qa/page/project/import/github.rb
@@ -8,8 +8,8 @@ module QA
include Page::Component::Select2
view 'app/views/import/github/new.html.haml' do
- element :personal_access_token_field, 'text_field_tag :personal_access_token' # rubocop:disable QA/ElementWithPattern
- element :authenticate_button, "submit_tag _('Authenticate')" # rubocop:disable QA/ElementWithPattern
+ element :personal_access_token_field
+ element :authenticate_button
end
view 'app/assets/javascripts/import_projects/components/provider_repo_table_row.vue' do
@@ -20,11 +20,9 @@ module QA
end
def add_personal_access_token(personal_access_token)
- fill_in 'personal_access_token', with: personal_access_token
- end
-
- def list_repos
- click_button 'List your GitHub repositories'
+ fill_element(:personal_access_token_field, personal_access_token)
+ click_element(:authenticate_button)
+ finished_loading?
end
def import!(full_path, name)
@@ -37,7 +35,7 @@ module QA
private
def within_repo_path(full_path)
- wait(reload: false) do
+ wait_until(reload: false) do
has_element?(:project_import_row, text: full_path)
end
@@ -69,7 +67,7 @@ module QA
end
def wait_for_success
- wait(max: 60, interval: 1.0, reload: false) do
+ wait_until(max_duration: 60, sleep_interval: 1.0, reload: false) do
page.has_content?('Done', wait: 1.0)
end
end
diff --git a/qa/qa/page/project/issue/index.rb b/qa/qa/page/project/issue/index.rb
index a6ccee4353b..b5ad63ab8de 100644
--- a/qa/qa/page/project/issue/index.rb
+++ b/qa/qa/page/project/issue/index.rb
@@ -10,6 +10,7 @@ module QA
end
view 'app/views/projects/issues/_issue.html.haml' do
+ element :issue
element :issue_link, 'link_to issue.title' # rubocop:disable QA/ElementWithPattern
end
@@ -21,10 +22,6 @@ module QA
element :closed_issues_link
end
- def assignee_link_count
- all_elements(:assignee_link).count
- end
-
def avatar_counter
find_element(:avatar_counter)
end
@@ -37,8 +34,12 @@ module QA
click_element :closed_issues_link
end
+ def has_assignee_link_count?(count)
+ all_elements(:assignee_link, count: count)
+ end
+
def has_issue?(issue)
- has_element? :issue, issue_title: issue.to_s
+ has_element? :issue, issue_title: issue.title
end
end
end
diff --git a/qa/qa/page/project/issue/show.rb b/qa/qa/page/project/issue/show.rb
index 1ef711d459e..a1e1bb4bc98 100644
--- a/qa/qa/page/project/issue/show.rb
+++ b/qa/qa/page/project/issue/show.rb
@@ -56,12 +56,6 @@ module QA
element :new_note_form, 'attr: :note' # rubocop:disable QA/ElementWithPattern
end
- def avatar_image_count
- wait_assignees_block_finish_loading do
- all_elements(:avatar_image).count
- end
- end
-
def click_milestone_link
click_element(:milestone_link)
end
@@ -88,12 +82,16 @@ module QA
click_element :comment_button
end
- def has_comment?(comment_text)
- wait(reload: false) do
- has_element?(:noteable_note_item, text: comment_text)
+ def has_avatar_image_count?(count)
+ wait_assignees_block_finish_loading do
+ all_elements(:avatar_image, count: count)
end
end
+ def has_comment?(comment_text)
+ has_element?(:noteable_note_item, text: comment_text, wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME)
+ end
+
def more_assignees_link
find_element(:more_assignees_link)
end
@@ -155,7 +153,7 @@ module QA
def wait_assignees_block_finish_loading
within_element(:assignee_block) do
- wait(reload: false, max: 10, interval: 1) do
+ wait_until(reload: false, max_duration: 10, sleep_interval: 1) do
finished_loading_block?
yield
end
diff --git a/qa/qa/page/project/job/show.rb b/qa/qa/page/project/job/show.rb
index a1a5b3c296e..07dea3449f1 100644
--- a/qa/qa/page/project/job/show.rb
+++ b/qa/qa/page/project/job/show.rb
@@ -5,8 +5,8 @@ module QA::Page
class Show < QA::Page::Base
include Component::CiBadgeLink
- view 'app/assets/javascripts/jobs/components/job_log.vue' do
- element :build_trace
+ view 'app/assets/javascripts/jobs/components/log/log.vue' do
+ element :job_log_content
end
view 'app/assets/javascripts/jobs/components/stages_dropdown.vue' do
@@ -24,8 +24,8 @@ module QA::Page
def output(wait: 5)
result = ''
- wait(reload: false, max: wait, interval: 1) do
- result = find_element(:build_trace).text
+ wait_until(reload: false, max_duration: wait, sleep_interval: 1) do
+ result = find_element(:job_log_content).text
result.include?('Job')
end
@@ -36,8 +36,8 @@ module QA::Page
private
def loaded?(wait: 60)
- wait(reload: true, max: wait, interval: 1) do
- has_element?(:build_trace, wait: 1)
+ wait_until(reload: true, max_duration: wait, sleep_interval: 1) do
+ has_element?(:job_log_content, wait: 1)
end
end
end
diff --git a/qa/qa/page/project/operations/environments/index.rb b/qa/qa/page/project/operations/environments/index.rb
index 610a34385b1..6b46fa4985a 100644
--- a/qa/qa/page/project/operations/environments/index.rb
+++ b/qa/qa/page/project/operations/environments/index.rb
@@ -11,9 +11,7 @@ module QA
end
def click_environment_link(environment_name)
- wait(reload: false) do
- find(element_selector_css(:environment_link), text: environment_name).click
- end
+ click_element(:environment_link, text: environment_name)
end
end
end
diff --git a/qa/qa/page/project/operations/kubernetes/index.rb b/qa/qa/page/project/operations/kubernetes/index.rb
index de54319596d..84b58e9ea5b 100644
--- a/qa/qa/page/project/operations/kubernetes/index.rb
+++ b/qa/qa/page/project/operations/kubernetes/index.rb
@@ -13,6 +13,10 @@ module QA
def add_kubernetes_cluster
click_on 'Add Kubernetes cluster'
end
+
+ def has_cluster?(cluster)
+ has_element?(:cluster, cluster_name: cluster.to_s)
+ end
end
end
end
diff --git a/qa/qa/page/project/operations/kubernetes/show.rb b/qa/qa/page/project/operations/kubernetes/show.rb
index fa276f15b8a..3d3eebdbec9 100644
--- a/qa/qa/page/project/operations/kubernetes/show.rb
+++ b/qa/qa/page/project/operations/kubernetes/show.rb
@@ -6,12 +6,6 @@ module QA
module Operations
module Kubernetes
class Show < Page::Base
- view 'app/assets/javascripts/clusters/components/application_row.vue' do
- element :application_row, 'js-cluster-application-row-${this.id}' # rubocop:disable QA/ElementWithPattern
- element :install_button, "__('Install')" # rubocop:disable QA/ElementWithPattern
- element :installed_button, "__('Installed')" # rubocop:disable QA/ElementWithPattern
- end
-
view 'app/assets/javascripts/clusters/components/applications.vue' do
element :ingress_ip_address, 'id="ingress-endpoint"' # rubocop:disable QA/ElementWithPattern
end
@@ -22,15 +16,21 @@ module QA
end
def install!(application_name)
- within(".js-cluster-application-row-#{application_name}") do
- page.has_button?('Install', wait: 30)
- click_on 'Install'
+ within_element(application_name) do
+ has_element?(:install_button, application: application_name, wait: 30)
+ click_on 'Install' # TODO replace with click_element
end
end
def await_installed(application_name)
- within(".js-cluster-application-row-#{application_name}") do
- page.has_text?(/Installed|Uninstall/, wait: 300)
+ within_element(application_name) do
+ has_element?(:uninstall_button, application: application_name, wait: 300)
+ end
+ end
+
+ def has_application_installed?(application_name)
+ within_element(application_name) do
+ has_element?(:uninstall_button, application: application_name, wait: 300)
end
end
diff --git a/qa/qa/page/project/pipeline/index.rb b/qa/qa/page/project/pipeline/index.rb
index 269d4dfc411..684ad4a59d5 100644
--- a/qa/qa/page/project/pipeline/index.rb
+++ b/qa/qa/page/project/pipeline/index.rb
@@ -18,7 +18,7 @@ module QA::Page
end
def wait_for_latest_pipeline_success
- wait(reload: false, max: 300) do
+ wait_until(reload: false, max_duration: 300) do
within_element_by_index(:pipeline_commit_status, 0) do
has_text?('passed')
end
diff --git a/qa/qa/page/project/pipeline/show.rb b/qa/qa/page/project/pipeline/show.rb
index fd29c5eacdc..45fffbf6000 100644
--- a/qa/qa/page/project/pipeline/show.rb
+++ b/qa/qa/page/project/pipeline/show.rb
@@ -67,13 +67,7 @@ module QA::Page
end
def click_on_first_job
- css = '.js-pipeline-graph-job-link'
-
- wait(reload: false) do
- has_css?(css)
- end
-
- first(css).click
+ first('.js-pipeline-graph-job-link', wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME).click
end
end
end
diff --git a/qa/qa/page/project/settings/ci_variables.rb b/qa/qa/page/project/settings/ci_variables.rb
index 3621e618bf2..64a182e5b3a 100644
--- a/qa/qa/page/project/settings/ci_variables.rb
+++ b/qa/qa/page/project/settings/ci_variables.rb
@@ -20,13 +20,13 @@ module QA
end
def fill_variable(key, value, masked)
- keys = all_elements(:ci_variable_input_key)
+ keys = all_elements(:ci_variable_input_key, minimum: 1)
index = keys.size - 1
# After we fill the key, JS would generate another field so
# we need to use the same index to find the corresponding one.
keys[index].set(key)
- node = all_elements(:ci_variable_input_value)[index]
+ node = all_elements(:ci_variable_input_value, count: keys.size + 1)[index]
# Simply run `node.set(value)` is too slow for long text here,
# so we need to run JavaScript directly to set the value.
@@ -34,7 +34,7 @@ module QA
# https://github.com/teamcapybara/capybara/blob/679548cea10773d45e32808f4d964377cfe5e892/lib/capybara/selenium/node.rb#L217
execute_script("arguments[0].value = #{value.to_json}", node)
- masked_node = all_elements(:variable_masked)[index]
+ masked_node = all_elements(:variable_masked, count: keys.size + 1)[index]
toggle_masked(masked_node, masked)
end
@@ -55,7 +55,7 @@ module QA
private
def toggle_masked(masked_node, masked)
- wait(reload: false) do
+ wait_until(reload: false) do
masked_node.click
masked ? masked_enabled?(masked_node) : masked_disabled?(masked_node)
diff --git a/qa/qa/page/project/settings/deploy_keys.rb b/qa/qa/page/project/settings/deploy_keys.rb
index 602bfc64710..c330d090ce6 100644
--- a/qa/qa/page/project/settings/deploy_keys.rb
+++ b/qa/qa/page/project/settings/deploy_keys.rb
@@ -18,7 +18,7 @@ module QA
view 'app/assets/javascripts/deploy_keys/components/key.vue' do
element :key
element :key_title
- element :key_fingerprint
+ element :key_md5_fingerprint
end
def add_key
@@ -33,17 +33,17 @@ module QA
fill_in 'deploy_key_key', with: key
end
- def find_fingerprint(title)
+ def find_md5_fingerprint(title)
within_project_deploy_keys do
find_element(:key, text: title)
- .find(element_selector_css(:key_fingerprint)).text
+ .find(element_selector_css(:key_md5_fingerprint)).text.delete_prefix('MD5:')
end
end
- def has_key?(title, fingerprint)
+ def has_key?(title, md5_fingerprint)
within_project_deploy_keys do
find_element(:key, text: title)
- .has_css?(element_selector_css(:key_fingerprint), text: fingerprint)
+ .has_css?(element_selector_css(:key_md5_fingerprint), text: "MD5:#{md5_fingerprint}")
end
end
@@ -53,18 +53,10 @@ module QA
end
end
- def key_fingerprint
- within_project_deploy_keys do
- find_element(:key_fingerprint).text
- end
- end
-
private
def within_project_deploy_keys
- wait(reload: false) do
- has_element?(:project_deploy_keys)
- end
+ has_element?(:project_deploy_keys, wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME)
within_element(:project_deploy_keys) do
yield
diff --git a/qa/qa/page/project/settings/deploy_tokens.rb b/qa/qa/page/project/settings/deploy_tokens.rb
index ad34ebc13c2..3173752d40a 100644
--- a/qa/qa/page/project/settings/deploy_tokens.rb
+++ b/qa/qa/page/project/settings/deploy_tokens.rb
@@ -51,9 +51,7 @@ module QA
private
def within_new_project_deploy_token
- wait(reload: false) do
- has_css?(element_selector_css(:created_deploy_token_section))
- end
+ has_element?(:created_deploy_token_section, wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME)
within_element(:created_deploy_token_section) do
yield
diff --git a/qa/qa/page/project/settings/members.rb b/qa/qa/page/project/settings/members.rb
index 2ef018fd983..fd3e0add2a6 100644
--- a/qa/qa/page/project/settings/members.rb
+++ b/qa/qa/page/project/settings/members.rb
@@ -8,9 +8,9 @@ module QA
include Page::Component::UsersSelect
include QA::Page::Component::Select2
- view 'app/views/projects/project_members/_new_project_member.html.haml' do
- element :member_select_input
- element :add_member_button
+ view 'app/views/shared/members/_invite_member.html.haml' do
+ element :member_select_field
+ element :invite_member_button
end
view 'app/views/projects/project_members/_team.html.haml' do
@@ -21,7 +21,7 @@ module QA
element :invite_group_tab
end
- view 'app/views/projects/project_members/_new_project_group.html.haml' do
+ view 'app/views/shared/members/_invite_group.html.haml' do
element :group_select_field
element :invite_group_button
end
@@ -43,8 +43,8 @@ module QA
end
def add_member(username)
- select_user :member_select_input, username
- click_element :add_member_button
+ select_user :member_select_field, username
+ click_element :invite_member_button
end
def remove_group(group_name)
diff --git a/qa/qa/page/project/settings/mirroring_repositories.rb b/qa/qa/page/project/settings/mirroring_repositories.rb
index 4afe042d9fb..517163a22f1 100644
--- a/qa/qa/page/project/settings/mirroring_repositories.rb
+++ b/qa/qa/page/project/settings/mirroring_repositories.rb
@@ -77,9 +77,7 @@ module QA
# The host key detection process is interrupted if we navigate away
# from the page before the fingerprint appears.
- wait(max: 5) do
- find_element(:fingerprints_list).has_text? /.*/
- end
+ find_element(:fingerprints_list, text: /.*/)
end
def mirror_repository
@@ -100,7 +98,7 @@ module QA
sleep 5
refresh
- wait(interval: 1) do
+ wait_until(sleep_interval: 1) do
within_element_by_index(:mirrored_repository_row, row_index) do
last_update = find_element(:mirror_last_update_at_cell, wait: 0)
last_update.has_text?('just now') || last_update.has_text?('seconds')
@@ -117,8 +115,8 @@ module QA
private
def find_repository_row_index(target_url)
- wait(max: 5, reload: false) do
- all_elements(:mirror_repository_url_cell).index do |url|
+ wait_until(max_duration: 5, reload: false) do
+ all_elements(:mirror_repository_url_cell, minimum: 1).index do |url|
# The url might be a sanitized url but the target_url won't be so
# we compare just the paths instead of the full url
URI.parse(url.text).path == target_url.path
diff --git a/qa/qa/page/project/settings/protected_branches.rb b/qa/qa/page/project/settings/protected_branches.rb
index d1d2f302013..f718311fbf2 100644
--- a/qa/qa/page/project/settings/protected_branches.rb
+++ b/qa/qa/page/project/settings/protected_branches.rb
@@ -46,7 +46,7 @@ module QA
end
def protect_branch
- click_element :protect_button
+ click_element(:protect_button, wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME)
end
private
@@ -58,10 +58,9 @@ module QA
within_element(:"allowed_to_#{action}_dropdown") do
click_on allowed[:roles]
+ allowed[:users].each { |user| click_on user.username } if allowed.key?(:users)
+ allowed[:groups].each { |group| click_on group.name } if allowed.key?(:groups)
end
-
- # Click the select element again to close the dropdown
- click_element :protected_branch_select
end
end
end
diff --git a/qa/qa/page/project/show.rb b/qa/qa/page/project/show.rb
index 102b6144a1e..c619bd6d6a3 100644
--- a/qa/qa/page/project/show.rb
+++ b/qa/qa/page/project/show.rb
@@ -61,9 +61,7 @@ module QA
end
def wait_for_viewers_to_load
- wait(reload: false) do
- has_no_element?(:spinner)
- end
+ has_no_element?(:spinner, wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME)
end
def create_first_new_file!
@@ -103,7 +101,7 @@ module QA
end
def new_merge_request
- wait(reload: true) do
+ wait_until(reload: true) do
has_css?(element_selector_css(:create_merge_request))
end
@@ -127,7 +125,7 @@ module QA
end
def wait_for_import
- wait(reload: true) do
+ wait_until(reload: true) do
has_css?('.tree-holder')
end
end
diff --git a/qa/qa/page/project/web_ide/edit.rb b/qa/qa/page/project/web_ide/edit.rb
index 4d26cadcdfe..73b0856b445 100644
--- a/qa/qa/page/project/web_ide/edit.rb
+++ b/qa/qa/page/project/web_ide/edit.rb
@@ -69,7 +69,7 @@ module QA
# Wait for the modal to fade out too
has_no_element?(:new_file_modal)
- wait(reload: false) do
+ wait_until(reload: false) do
within_element(:file_templates_bar) do
click_element :file_template_dropdown
fill_element :dropdown_filter_input, template
@@ -84,35 +84,29 @@ module QA
end
def commit_changes
- # Clicking :begin_commit_button the first time switches from the
+ # Clicking :begin_commit_button switches from the
# edit to the commit view
click_element :begin_commit_button
active_element? :commit_mode_tab
- # We need to click :begin_commit_button again
- click_element :begin_commit_button
-
- # After clicking :begin_commit_button the 2nd time there is an
- # animation that hides :begin_commit_button and shows :commit_button
+ # After clicking :begin_commit_button, there is an animation
+ # that hides :begin_commit_button and shows :commit_button
#
# Wait for the animation to complete before clicking :commit_button
# otherwise the click will quietly do nothing.
- wait(reload: false) do
+ wait_until(reload: false) do
has_no_element?(:begin_commit_button) &&
has_element?(:commit_button)
end
- # At this point we're ready to commit and the button should be
- # labelled "Stage & Commit"
- #
# Click :commit_button and keep retrying just in case part of the
# animation is still in process even when the buttons have the
# expected visibility.
- commit_success_msg_shown = retry_until do
- click_element :commit_to_current_branch_radio
- click_element :commit_button
+ commit_success_msg_shown = retry_until(sleep_interval: 5) do
+ click_element(:commit_to_current_branch_radio) if has_element?(:commit_to_current_branch_radio)
+ click_element(:commit_button) if has_element?(:commit_button)
- wait(reload: false) do
+ wait_until(reload: false) do
has_text?('Your changes have been committed')
end
end
diff --git a/qa/qa/page/search/results.rb b/qa/qa/page/search/results.rb
index 2f99d8da784..85f1d224935 100644
--- a/qa/qa/page/search/results.rb
+++ b/qa/qa/page/search/results.rb
@@ -19,11 +19,11 @@ module QA::Page
end
def switch_to_code
- click_element(:code_tab)
+ switch_to_tab(:code_tab)
end
def switch_to_projects
- click_element(:projects_tab)
+ switch_to_tab(:projects_tab)
end
def has_file_in_project?(file_name, project_name)
@@ -32,7 +32,7 @@ module QA::Page
def has_file_with_content?(file_name, file_text)
within_element_by_index(:result_item_content, 0) do
- false unless has_element?(:file_title_content, text: file_name)
+ break false unless has_element?(:file_title_content, text: file_name)
has_element?(:file_text_content, text: file_text)
end
@@ -41,6 +41,15 @@ module QA::Page
def has_project?(project_name)
has_element?(:project, project_name: project_name)
end
+
+ private
+
+ def switch_to_tab(tab)
+ retry_until do
+ click_element(tab)
+ has_active_element?(tab)
+ end
+ end
end
end
end
diff --git a/qa/qa/page/settings/common.rb b/qa/qa/page/settings/common.rb
index bd1070158f0..6989e8125d3 100644
--- a/qa/qa/page/settings/common.rb
+++ b/qa/qa/page/settings/common.rb
@@ -10,7 +10,7 @@ module QA
def expand_section(element_name)
within_element(element_name) do
# Because it is possible to click the button before the JS toggle code is bound
- wait(reload: false) do
+ wait_until(reload: false) do
click_button 'Expand' unless has_css?('button', text: 'Collapse', wait: 1)
has_content?('Collapse')
diff --git a/qa/qa/page/validatable.rb b/qa/qa/page/validatable.rb
index 8467d261285..3c4d9ad68aa 100644
--- a/qa/qa/page/validatable.rb
+++ b/qa/qa/page/validatable.rb
@@ -11,8 +11,7 @@ module QA
elements.each do |element|
next unless element.required?
- # TODO: this wait needs to be replaced by the wait class
- unless base_page.has_element?(element.name, wait: 60)
+ unless base_page.has_element?(element.name, wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME)
raise Validatable::PageValidationError, "#{element.name} did not appear on #{self.name} as expected"
end
end
diff --git a/qa/qa/resource/base.rb b/qa/qa/resource/base.rb
index 3bb62703290..873ba353051 100644
--- a/qa/qa/resource/base.rb
+++ b/qa/qa/resource/base.rb
@@ -66,18 +66,24 @@ module QA
def visit!
Runtime::Logger.debug(%Q[Visiting #{self.class.name} at "#{web_url}"])
+ # Just in case an async action is not yet complete
+ Support::WaitForRequests.wait_for_requests
+
Support::Retrier.retry_until do
visit(web_url)
- wait { current_url.include?(URI.parse(web_url).path.split('/').last || web_url) }
+ wait_until { current_url.include?(URI.parse(web_url).path.split('/').last || web_url) }
end
+
+ # Wait until the new page is ready for us to interact with it
+ Support::WaitForRequests.wait_for_requests
end
def populate(*attributes)
attributes.each(&method(:public_send))
end
- def wait(max: 60, interval: 0.1)
- QA::Support::Waiter.wait(max: max, interval: interval) do
+ def wait_until(max_duration: 60, sleep_interval: 0.1)
+ QA::Support::Waiter.wait_until(max_duration: max_duration, sleep_interval: sleep_interval) do
yield
end
end
diff --git a/qa/qa/resource/deploy_key.rb b/qa/qa/resource/deploy_key.rb
index 869e2a71e47..26355729dab 100644
--- a/qa/qa/resource/deploy_key.rb
+++ b/qa/qa/resource/deploy_key.rb
@@ -5,10 +5,10 @@ module QA
class DeployKey < Base
attr_accessor :title, :key
- attribute :fingerprint do
+ attribute :md5_fingerprint do
Page::Project::Settings::Repository.perform do |setting|
setting.expand_deploy_keys do |key|
- key.find_fingerprint(title)
+ key.find_md5_fingerprint(title)
end
end
end
diff --git a/qa/qa/resource/events/base.rb b/qa/qa/resource/events/base.rb
index b50b620b143..f98a54a6f57 100644
--- a/qa/qa/resource/events/base.rb
+++ b/qa/qa/resource/events/base.rb
@@ -4,6 +4,7 @@ module QA
module Resource
module Events
MAX_WAIT = 10
+ RAISE_ON_FAILURE = true
EventNotFoundError = Class.new(RuntimeError)
@@ -21,7 +22,7 @@ module QA
end
def wait_for_event
- event_found = QA::Support::Waiter.wait(max: max_wait) do
+ event_found = Support::Waiter.wait_until(max_duration: max_wait, raise_on_failure: raise_on_failure) do
yield
end
@@ -31,6 +32,10 @@ module QA
def max_wait
MAX_WAIT
end
+
+ def raise_on_failure
+ RAISE_ON_FAILURE
+ end
end
end
end
diff --git a/qa/qa/resource/group.rb b/qa/qa/resource/group.rb
index c12e9dd146b..0824512d238 100644
--- a/qa/qa/resource/group.rb
+++ b/qa/qa/resource/group.rb
@@ -39,7 +39,7 @@ module QA
end
# Ensure that the group was actually created
- group_show.wait(interval: 1) do
+ group_show.wait_until(sleep_interval: 1) do
group_show.has_text?(path) &&
group_show.has_new_project_or_subgroup_dropdown?
end
diff --git a/qa/qa/resource/issue.rb b/qa/qa/resource/issue.rb
index 3bcff6a10ac..0817a9de06f 100644
--- a/qa/qa/resource/issue.rb
+++ b/qa/qa/resource/issue.rb
@@ -38,10 +38,6 @@ module QA
end
end
- def to_s
- @title
- end
-
def api_get_path
"/projects/#{project.id}/issues/#{id}"
end
diff --git a/qa/qa/resource/merge_request.rb b/qa/qa/resource/merge_request.rb
index 24fb96a20a2..6c0f4621dd9 100644
--- a/qa/qa/resource/merge_request.rb
+++ b/qa/qa/resource/merge_request.rb
@@ -54,7 +54,7 @@ module QA
@assignee = nil
@milestone = nil
@labels = []
- @file_name = "added_file.txt"
+ @file_name = "added_file-#{SecureRandom.hex(8)}.txt"
@file_content = "File Added"
@target_new_branch = true
@no_preparation = false
diff --git a/qa/qa/resource/merge_request_from_fork.rb b/qa/qa/resource/merge_request_from_fork.rb
index 6c9a096289b..9cb4e6a49ca 100644
--- a/qa/qa/resource/merge_request_from_fork.rb
+++ b/qa/qa/resource/merge_request_from_fork.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+require 'securerandom'
+
module QA
module Resource
class MergeRequestFromFork < MergeRequest
@@ -13,7 +15,7 @@ module QA
Repository::ProjectPush.fabricate! do |resource|
resource.project = fork.project
resource.branch_name = fork_branch
- resource.file_name = 'file2.txt'
+ resource.file_name = "file2-#{SecureRandom.hex(8)}.txt"
resource.user = fork.user
end
end
diff --git a/qa/qa/resource/project_imported_from_github.rb b/qa/qa/resource/project_imported_from_github.rb
index 3e25235e6b8..e5ecaeae139 100644
--- a/qa/qa/resource/project_imported_from_github.rb
+++ b/qa/qa/resource/project_imported_from_github.rb
@@ -23,7 +23,6 @@ module QA
Page::Project::Import::Github.perform do |import_page|
import_page.add_personal_access_token(@personal_access_token)
- import_page.list_repos
import_page.import!(@github_repository_path, @name)
end
end
diff --git a/qa/qa/resource/protected_branch.rb b/qa/qa/resource/protected_branch.rb
index f0cef624e0b..9c65e0e5a31 100644
--- a/qa/qa/resource/protected_branch.rb
+++ b/qa/qa/resource/protected_branch.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+require 'securerandom'
+
module QA
module Resource
class ProtectedBranch < Base
@@ -15,7 +17,7 @@ module QA
attribute :branch do
Repository::ProjectPush.fabricate! do |project_push|
project_push.project = project
- project_push.file_name = 'new_file.md'
+ project_push.file_name = "new_file-#{SecureRandom.hex(8)}.md"
project_push.commit_message = 'Add new file'
project_push.branch_name = branch_name
project_push.new_branch = true
@@ -47,11 +49,6 @@ module QA
page.select_branch(branch_name)
page.select_allowed_to_merge(allowed_to_merge)
page.select_allowed_to_push(allowed_to_push)
-
- page.wait(reload: false) do
- !page.first('.btn-success').disabled?
- end
-
page.protect_branch
end
end
diff --git a/qa/qa/resource/repository/commit.rb b/qa/qa/resource/repository/commit.rb
index 4b5e8535ade..e3fb5bf486d 100644
--- a/qa/qa/resource/repository/commit.rb
+++ b/qa/qa/resource/repository/commit.rb
@@ -11,6 +11,8 @@ module QA
:file_path,
:sha
+ attribute :short_id
+
attribute :project do
Project.fabricate! do |resource|
resource.name = 'project-with-commit'
diff --git a/qa/qa/resource/repository/project_push.rb b/qa/qa/resource/repository/project_push.rb
index f79bb035c46..17596601cf9 100644
--- a/qa/qa/resource/repository/project_push.rb
+++ b/qa/qa/resource/repository/project_push.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+require 'securerandom'
+
module QA
module Resource
module Repository
@@ -15,7 +17,7 @@ module QA
end
def initialize
- @file_name = 'file.txt'
+ @file_name = "file-#{SecureRandom.hex(8)}.txt"
@file_content = '# This is test project'
@commit_message = "This is a test commit"
@branch_name = 'master'
diff --git a/qa/qa/resource/repository/push.rb b/qa/qa/resource/repository/push.rb
index 68674248be2..902ae9f3135 100644
--- a/qa/qa/resource/repository/push.rb
+++ b/qa/qa/resource/repository/push.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
require 'pathname'
+require 'securerandom'
module QA
module Resource
@@ -13,7 +14,7 @@ module QA
attr_writer :remote_branch, :gpg_key_id
def initialize
- @file_name = 'file.txt'
+ @file_name = "file-#{SecureRandom.hex(8)}.txt"
@file_content = '# This is test file'
@commit_message = "This is a test commit"
@branch_name = 'master'
diff --git a/qa/qa/resource/runner.rb b/qa/qa/resource/runner.rb
index d1b4e8f7d54..f1f72c9cacd 100644
--- a/qa/qa/resource/runner.rb
+++ b/qa/qa/resource/runner.rb
@@ -54,7 +54,7 @@ module QA
@id = this_runner[:id]
super
-
+ ensure
Service::DockerRun::GitlabRunner.new(name).remove!
end
diff --git a/qa/qa/resource/sandbox.rb b/qa/qa/resource/sandbox.rb
index 6c87fcb377a..54c13071cef 100644
--- a/qa/qa/resource/sandbox.rb
+++ b/qa/qa/resource/sandbox.rb
@@ -41,6 +41,14 @@ module QA
resource_web_url(api_get)
rescue ResourceNotFoundError
super
+
+ # If the group was just created the runners token might not be
+ # available via the API immediately.
+ Support::Retrier.retry_on_exception(sleep_interval: 5) do
+ resource = resource_web_url(api_get)
+ populate(:runners_token)
+ resource
+ end
end
def api_get_path
diff --git a/qa/qa/resource/ssh_key.rb b/qa/qa/resource/ssh_key.rb
index c140cb9ca62..22bdea424ca 100644
--- a/qa/qa/resource/ssh_key.rb
+++ b/qa/qa/resource/ssh_key.rb
@@ -7,7 +7,7 @@ module QA
attr_accessor :title
- def_delegators :key, :private_key, :public_key, :fingerprint
+ def_delegators :key, :private_key, :public_key, :md5_fingerprint
def key
@key ||= Runtime::Key::RSA.new
diff --git a/qa/qa/runtime/application_settings.rb b/qa/qa/runtime/application_settings.rb
new file mode 100644
index 00000000000..df6323f9a48
--- /dev/null
+++ b/qa/qa/runtime/application_settings.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module QA
+ module Runtime
+ module ApplicationSettings
+ extend self
+ extend Support::Api
+
+ APPLICATION_SETTINGS_PATH = '/application/settings'
+
+ # Set a GitLab application setting
+ # Example:
+ # #set({ allow_local_requests_from_web_hooks_and_services: true })
+ # #set(allow_local_requests_from_web_hooks_and_services: true)
+ # https://docs.gitlab.com/ee/api/settings.html
+ def set_application_settings(**application_settings)
+ QA::Runtime::Logger.info("Setting application settings: #{application_settings}")
+ r = put(Runtime::API::Request.new(api_client, APPLICATION_SETTINGS_PATH).url, **application_settings)
+ raise "Couldn't set application settings #{application_settings.inspect}" unless r.code == QA::Support::Api::HTTP_STATUS_OK
+ end
+
+ def get_application_settings
+ parse_body(get(Runtime::API::Request.new(api_client, APPLICATION_SETTINGS_PATH).url))
+ end
+
+ private
+
+ def api_client
+ @api_client ||= begin
+ return Runtime::API::Client.new(:gitlab, personal_access_token: Runtime::Env.admin_personal_access_token) if Runtime::Env.admin_personal_access_token
+
+ user = Resource::User.fabricate_via_api! do |user|
+ user.username = Runtime::User.admin_username
+ user.password = Runtime::User.admin_password
+ end
+
+ unless user.admin?
+ raise "Administrator access is required to set application settings. User '#{user.username}' is not an administrator."
+ end
+
+ Runtime::API::Client.new(:gitlab, user: user)
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/runtime/browser.rb b/qa/qa/runtime/browser.rb
index 69ba90702be..340f6dc0356 100644
--- a/qa/qa/runtime/browser.rb
+++ b/qa/qa/runtime/browser.rb
@@ -15,6 +15,10 @@ module QA
CAPYBARA_MAX_WAIT_TIME = 10
+ class << self
+ attr_accessor :rspec_configured, :capybara_configured
+ end
+
def initialize
self.class.configure!
end
@@ -45,11 +49,40 @@ module QA
end
def self.configure!
+ configure_rspec!
+ configure_capybara!
+ end
+
+ def self.configure_rspec!
+ # We don't want to enter this infinite loop:
+ # Runtime::Release.perform_before_hooks -> `QA::Runtime::Browser.visit` -> configure! -> configure_rspec! -> Runtime::Release.perform_before_hooks
+ # So we make sure this method is called only once.
+ return if self.rspec_configured
+
+ browser = self
+
RSpec.configure do |config|
config.define_derived_metadata(file_path: %r{/qa/specs/features/}) do |metadata|
metadata[:type] = :feature
end
+
+ config.before do
+ unless browser.rspec_configured
+ browser.rspec_configured = true
+
+ ##
+ # Perform before hooks, which are different for CE and EE
+ #
+ Runtime::Release.perform_before_hooks
+ end
+ end
end
+ end
+
+ def self.configure_capybara!
+ return if self.capybara_configured
+
+ self.capybara_configured = true
Capybara.server_port = 9887 + ENV['TEST_ENV_NUMBER'].to_i
diff --git a/qa/qa/runtime/env.rb b/qa/qa/runtime/env.rb
index 184ccd3ef07..6514e41e279 100644
--- a/qa/qa/runtime/env.rb
+++ b/qa/qa/runtime/env.rb
@@ -248,6 +248,10 @@ module QA
raise ArgumentError, "Please provide GITHUB_ACCESS_TOKEN"
end
+ def require_admin_access_token!
+ admin_personal_access_token || (raise ArgumentError, "GITLAB_QA_ADMIN_ACCESS_TOKEN is required!")
+ end
+
# Returns true if there is an environment variable that indicates that
# the feature is supported in the environment under test.
# All features are supported by default.
diff --git a/qa/qa/runtime/feature.rb b/qa/qa/runtime/feature.rb
index 8c19436ee12..25fc02a887e 100644
--- a/qa/qa/runtime/feature.rb
+++ b/qa/qa/runtime/feature.rb
@@ -33,7 +33,7 @@ module QA
is_enabled = false
- QA::Support::Waiter.wait(interval: 1) do
+ QA::Support::Waiter.wait_until(sleep_interval: 1) do
is_enabled = enabled?(key)
end
diff --git a/qa/qa/runtime/key/base.rb b/qa/qa/runtime/key/base.rb
index 1281eceaff0..72d1673438a 100644
--- a/qa/qa/runtime/key/base.rb
+++ b/qa/qa/runtime/key/base.rb
@@ -4,7 +4,7 @@ module QA
module Runtime
module Key
class Base
- attr_reader :name, :bits, :private_key, :public_key, :fingerprint
+ attr_reader :name, :bits, :private_key, :public_key, :md5_fingerprint
def initialize(name, bits)
@name = name
@@ -29,7 +29,7 @@ module QA
def populate_key_data(path)
@private_key = ::File.binread(path)
@public_key = ::File.binread("#{path}.pub")
- @fingerprint =
+ @md5_fingerprint =
`ssh-keygen -l -E md5 -f #{path} | cut -d' ' -f2 | cut -d: -f2-`.chomp
end
end
diff --git a/qa/qa/runtime/logger.rb b/qa/qa/runtime/logger.rb
index 7f73f1bd01b..a70c8faf7d2 100644
--- a/qa/qa/runtime/logger.rb
+++ b/qa/qa/runtime/logger.rb
@@ -14,11 +14,9 @@ module QA
attr_writer :logger
def logger
- return @logger if @logger
-
- @logger = ::Logger.new Runtime::Env.log_destination
- @logger.level = Runtime::Env.debug? ? ::Logger::DEBUG : ::Logger::ERROR
- @logger
+ @logger ||= ::Logger.new(Runtime::Env.log_destination).tap do |logger|
+ logger.level = Runtime::Env.debug? ? ::Logger::DEBUG : ::Logger::ERROR
+ end
end
end
end
diff --git a/qa/qa/runtime/search.rb b/qa/qa/runtime/search.rb
index faa110c96e7..74402301098 100644
--- a/qa/qa/runtime/search.rb
+++ b/qa/qa/runtime/search.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+require 'securerandom'
+
module QA
module Runtime
module Search
@@ -8,26 +10,83 @@ module QA
ElasticSearchServerError = Class.new(RuntimeError)
- def elasticsearch_responding?
+ def assert_elasticsearch_responding
QA::Runtime::Logger.debug("Attempting to search via Elasticsearch...")
- QA::Support::Retrier.retry_on_exception do
- # We don't care about the results of the search, we just need
- # any search that uses Elasticsearch, not the native search
- # The Elasticsearch-only scopes are blobs, wiki_blobs, and commits.
- request = Runtime::API::Request.new(api_client, "/search?scope=blobs&search=foo")
- response = get(request.url)
+ QA::Support::Retrier.retry_on_exception(max_attempts: 3) do
+ search_term = SecureRandom.hex(8)
+
+ QA::Runtime::Logger.debug("Creating commit and project including search term '#{search_term}'...")
- unless response.code == singleton_class::HTTP_STATUS_OK
- raise ElasticSearchServerError, "Search attempt failed. Request returned (#{response.code}): `#{response}`."
+ content = "Elasticsearch test commit #{search_term}"
+ project = Resource::Project.fabricate_via_api! do |project|
+ project.name = "project-to-search-#{search_term}"
+ end
+ commit = Resource::Repository::Commit.fabricate_via_api! do |commit|
+ commit.project = project
+ commit.commit_message = content
+ commit.add_files(
+ [
+ {
+ file_path: 'test.txt',
+ content: content
+ }
+ ]
+ )
end
- true
+ find_commit(commit, "commit*#{search_term}")
+ find_project(project, "to-search*#{search_term}")
+ end
+ end
+
+ def find_code(file_name, search_term)
+ find_target_in_scope('blobs', search_term) do |record|
+ record[:filename] == file_name && record[:data].include?(search_term)
end
+
+ QA::Runtime::Logger.debug("Found file '#{file_name} containing code '#{search_term}'")
+ end
+
+ def find_commit(commit, search_term)
+ find_target_in_scope('commits', search_term) do |record|
+ record[:message] == commit.commit_message
+ end
+
+ QA::Runtime::Logger.debug("Found commit '#{commit.commit_message} (#{commit.short_id})' via '#{search_term}'")
+ end
+
+ def find_project(project, search_term)
+ find_target_in_scope('projects', search_term) do |record|
+ record[:name] == project.name
+ end
+
+ QA::Runtime::Logger.debug("Found project '#{project.name}' via '#{search_term}'")
end
private
+ def find_target_in_scope(scope, search_term)
+ QA::Support::Retrier.retry_until(max_attempts: 10, sleep_interval: 10, raise_on_failure: true, retry_on_exception: true) do
+ result = search(scope, search_term)
+ result && result.any? { |record| yield record }
+ end
+ end
+
+ def search(scope, term)
+ QA::Runtime::Logger.debug("Search scope '#{scope}' for '#{term}'...")
+ request = Runtime::API::Request.new(api_client, "/search?scope=#{scope}&search=#{term}")
+ response = get(request.url)
+
+ unless response.code == singleton_class::HTTP_STATUS_OK
+ msg = "Search attempt failed. Request returned (#{response.code}): `#{response}`."
+ QA::Runtime::Logger.debug(msg)
+ raise ElasticSearchServerError, msg
+ end
+
+ parse_body(response)
+ end
+
def api_client
@api_client ||= Runtime::API::Client.new(:gitlab)
end
diff --git a/qa/qa/scenario/template.rb b/qa/qa/scenario/template.rb
index 74d4c8f8757..97373f7a059 100644
--- a/qa/qa/scenario/template.rb
+++ b/qa/qa/scenario/template.rb
@@ -23,11 +23,6 @@ module QA
def perform(options, *args)
extract_address(:gitlab_address, options, args)
- ##
- # Perform before hooks, which are different for CE and EE
- #
- Runtime::Release.perform_before_hooks
-
Runtime::Feature.enable(options[:enable_feature]) if options.key?(:enable_feature)
Specs::Runner.perform do |specs|
diff --git a/qa/qa/scenario/test/instance.rb b/qa/qa/scenario/test/instance.rb
index b4098619e4e..79dad7f4619 100644
--- a/qa/qa/scenario/test/instance.rb
+++ b/qa/qa/scenario/test/instance.rb
@@ -20,11 +20,6 @@ module QA
def self.do_perform(address, *rspec_options)
Runtime::Scenario.define(:gitlab_address, address)
- ##
- # Perform before hooks, which are different for CE and EE
- #
- Runtime::Release.perform_before_hooks
-
Specs::Runner.perform do |specs|
specs.tty = true
specs.options = rspec_options if rspec_options.any?
diff --git a/qa/qa/service/cluster_provider/k3d.rb b/qa/qa/service/cluster_provider/k3d.rb
index 8e117c2dbd5..fe02dde607c 100644
--- a/qa/qa/service/cluster_provider/k3d.rb
+++ b/qa/qa/service/cluster_provider/k3d.rb
@@ -6,6 +6,8 @@ module QA
class K3d < Base
def validate_dependencies
find_executable('k3d') || raise("You must first install `k3d` executable to run these tests.")
+ Runtime::Env.require_admin_access_token!
+ Runtime::ApplicationSettings.set_application_settings(allow_local_requests_from_web_hooks_and_services: true)
end
def set_credentials(admin_user)
@@ -24,6 +26,7 @@ module QA
def teardown
ENV['KUBECONFIG'] = @old_kubeconfig
shell "k3d delete --name #{cluster_name}"
+ Runtime::ApplicationSettings.set_application_settings(allow_local_requests_from_web_hooks_and_services: false)
end
# Fetch "real" certificate
diff --git a/qa/qa/service/kubernetes_cluster.rb b/qa/qa/service/kubernetes_cluster.rb
index 26b5f58d2d3..84196556547 100644
--- a/qa/qa/service/kubernetes_cluster.rb
+++ b/qa/qa/service/kubernetes_cluster.rb
@@ -39,6 +39,10 @@ module QA
@provider.cluster_name
end
+ def to_s
+ cluster_name
+ end
+
private
def fetch_api_url
diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/login_via_oauth_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/login_via_oauth_spec.rb
index 4fd80c353fb..70303a30153 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/login/login_via_oauth_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/login/login_via_oauth_spec.rb
@@ -1,8 +1,7 @@
# frozen_string_literal: true
module QA
- # Failure issue: https://gitlab.com/gitlab-org/gitlab/issues/36305
- context 'Manage', :orchestrated, :oauth, :skip do
+ context 'Manage', :orchestrated, :oauth, quarantine: 'https://gitlab.com/gitlab-org/gitlab/issues/196517' do
describe 'OAuth login' do
it 'User logs in to GitLab with GitHub OAuth' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
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 e0045a4d8a1..14eaf770f10 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
@@ -21,6 +21,8 @@ module QA
delete delete_project_request.url
expect_status(202)
+
+ Page::Main::Menu.perform(&:sign_out_if_signed_in)
end
it 'user imports a GitHub repo' do
diff --git a/qa/qa/specs/features/browser_ui/2_plan/email/trigger_email_notification_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/email/trigger_email_notification_spec.rb
index 90290b4f2a0..eecf485a518 100644
--- a/qa/qa/specs/features/browser_ui/2_plan/email/trigger_email_notification_spec.rb
+++ b/qa/qa/specs/features/browser_ui/2_plan/email/trigger_email_notification_spec.rb
@@ -22,8 +22,15 @@ module QA
expect(page).to have_content(/@#{user.username}(\n| )?Given access/)
- # Wait for Action Mailer to deliver messages
- mailhog_json = Support::Retrier.retry_until(sleep_interval: 1) do
+ mailhog_items = mailhog_json.dig('items')
+
+ expect(mailhog_items).to include(an_object_satisfying { |o| /project was granted/ === o.dig('Content', 'Headers', 'Subject', 0) })
+ end
+
+ private
+
+ def mailhog_json
+ Support::Retrier.retry_until(sleep_interval: 1) do
Runtime::Logger.debug(%Q[retrieving "#{QA::Runtime::MailHog.api_messages_url}"])
mailhog_response = get QA::Runtime::MailHog.api_messages_url
@@ -33,10 +40,6 @@ module QA
# Expect at least two invitation messages: group and project
mailhog_data if mailhog_data.dig('total') >= 2
end
-
- # Check json result from mailhog
- mailhog_items = mailhog_json.dig('items')
- expect(mailhog_items).to include(an_object_satisfying { |o| /project was granted/ === o.dig('Content', 'Headers', 'Subject', 0) })
end
end
end
diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/check_mentions_for_xss_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/check_mentions_for_xss_spec.rb
index 494108dbefc..aa88937504e 100644
--- a/qa/qa/specs/features/browser_ui/2_plan/issue/check_mentions_for_xss_spec.rb
+++ b/qa/qa/specs/features/browser_ui/2_plan/issue/check_mentions_for_xss_spec.rb
@@ -24,25 +24,19 @@ module QA
project = Resource::Project.fabricate_via_api! do |resource|
resource.name = 'xss-test-for-mentions-project'
end
- project.visit!
- Page::Project::Show.perform(&:go_to_members_settings)
- Page::Project::Settings::Members.perform do |members|
- members.add_member(user.username)
- end
+ Flow::Project.add_member(project: project, username: user.username)
- issue = Resource::Issue.fabricate_via_api! do |issue|
- issue.title = 'issue title'
+ Resource::Issue.fabricate_via_api! do |issue|
issue.project = project
- end
- issue.visit!
+ end.visit!
Page::Project::Issue::Show.perform do |show|
show.select_all_activities_filter
show.comment("cc-ing you here @#{user.username}")
expect do
- expect(show).to have_content("cc-ing you here")
+ expect(show).to have_comment("cc-ing you here")
end.not_to raise_error # Selenium::WebDriver::Error::UnhandledAlertError
end
end
diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/close_issue_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/close_issue_spec.rb
index bab6b1ac5fc..2543c0091fb 100644
--- a/qa/qa/specs/features/browser_ui/2_plan/issue/close_issue_spec.rb
+++ b/qa/qa/specs/features/browser_ui/2_plan/issue/close_issue_spec.rb
@@ -4,12 +4,9 @@ module QA
context 'Plan' do
describe 'Close issue' do
let(:issue) do
- Resource::Issue.fabricate_via_api! do |issue|
- issue.title = 'Issue to be closed via pushing a commit'
- end
+ Resource::Issue.fabricate_via_api!
end
- let(:project) { issue.project }
let(:issue_id) { issue.api_response[:iid] }
before do
@@ -27,7 +24,7 @@ module QA
issue.visit!
Page::Project::Issue::Show.perform do |show|
- reopen_issue_button_visible = show.wait(reload: true) do
+ reopen_issue_button_visible = show.wait_until(reload: true) do
show.has_element?(:reopen_issue_button, wait: 1.0)
end
expect(reopen_issue_button_visible).to be_truthy
@@ -39,7 +36,7 @@ module QA
push.commit_message = commit_message
push.new_branch = new_branch
push.file_content = commit_message
- push.project = project
+ push.project = issue.project
end
end
end
diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/collapse_comments_in_discussions_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/collapse_comments_in_discussions_spec.rb
index 77fcc4e9b6a..e505c0991a6 100644
--- a/qa/qa/specs/features/browser_ui/2_plan/issue/collapse_comments_in_discussions_spec.rb
+++ b/qa/qa/specs/features/browser_ui/2_plan/issue/collapse_comments_in_discussions_spec.rb
@@ -8,20 +8,12 @@ module QA
before do
Flow::Login.sign_in
- issue = Resource::Issue.fabricate_via_api! do |issue|
- issue.title = 'issue title'
- end
-
- issue.visit!
+ Resource::Issue.fabricate_via_api!.visit!
Page::Project::Issue::Show.perform do |show|
- my_first_discussion = 'My first discussion'
-
show.select_all_activities_filter
- show.start_discussion(my_first_discussion)
- page.assert_text(my_first_discussion)
- show.reply_to_discussion(my_first_reply)
- page.assert_text(my_first_reply)
+ show.start_discussion('My first discussion')
+ show.reply_to_discussion(1, my_first_reply)
end
end
diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/comment_issue_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/comment_issue_spec.rb
index 77489c0ecf5..6c37e3ecbb9 100644
--- a/qa/qa/specs/features/browser_ui/2_plan/issue/comment_issue_spec.rb
+++ b/qa/qa/specs/features/browser_ui/2_plan/issue/comment_issue_spec.rb
@@ -6,10 +6,7 @@ module QA
before do
Flow::Login.sign_in
- issue = Resource::Issue.fabricate_via_api! do |issue|
- issue.title = 'issue title'
- end
- issue.visit!
+ Resource::Issue.fabricate_via_api!.visit!
end
it 'user comments on an issue and edits the comment' do
diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb
index 254efb741b3..3b231b9930e 100644
--- a/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb
+++ b/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb
@@ -3,16 +3,12 @@
module QA
context 'Plan', :smoke do
describe 'Issue creation' do
- let(:issue_title) { 'issue title' }
-
before do
Flow::Login.sign_in
end
it 'user creates an issue' do
- issue = Resource::Issue.fabricate_via_browser_ui! do |issue|
- issue.title = issue_title
- end
+ issue = Resource::Issue.fabricate_via_browser_ui!
Page::Project::Menu.perform(&:click_issues)
@@ -28,11 +24,7 @@ module QA
end
before do
- issue = Resource::Issue.fabricate_via_api! do |issue|
- issue.title = issue_title
- end
-
- issue.visit!
+ Resource::Issue.fabricate_via_api!.visit!
end
it 'user comments on an issue with an attachment' do
diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb
index a4f6b0bb1bf..4156ba54785 100644
--- a/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb
+++ b/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb
@@ -3,16 +3,10 @@
module QA
context 'Plan' do
describe 'filter issue comments activities' do
- let(:issue_title) { 'issue title' }
-
before do
Flow::Login.sign_in
- issue = Resource::Issue.fabricate_via_api! do |issue|
- issue.title = issue_title
- end
-
- issue.visit!
+ Resource::Issue.fabricate_via_api!.visit!
end
it 'user filters comments and activities in an issue' do
diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/mentions_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/mentions_spec.rb
index b1a80ad75cd..a0647df4097 100644
--- a/qa/qa/specs/features/browser_ui/2_plan/issue/mentions_spec.rb
+++ b/qa/qa/specs/features/browser_ui/2_plan/issue/mentions_spec.rb
@@ -12,18 +12,12 @@ module QA
resource.name = 'project-to-test-mention'
resource.visibility = 'private'
end
- project.visit!
- Page::Project::Show.perform(&:go_to_members_settings)
- Page::Project::Settings::Members.perform do |members|
- members.add_member(@user.username)
- end
+ project.add_member(@user)
- issue = Resource::Issue.fabricate_via_api! do |issue|
- issue.title = 'issue to test mention'
+ Resource::Issue.fabricate_via_api! do |issue|
issue.project = project
- end
- issue.visit!
+ end.visit!
end
it 'user mentions another user in an issue' do
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 0eaec61b2fa..604b6c10aee 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
@@ -4,12 +4,7 @@ module QA
context 'Create' do
describe 'Download merge request patch and diff' do
before(:context) do
- project = Resource::Project.fabricate_via_api! do |project|
- project.name = 'project'
- end
-
@merge_request = Resource::MergeRequest.fabricate_via_api! do |merge_request|
- merge_request.project = project
merge_request.title = 'This is a merge request'
merge_request.description = '... for downloading patches and diffs'
end
@@ -23,7 +18,7 @@ module QA
expect(page.text).to start_with('From')
expect(page).to have_content('Subject: [PATCH] This is a test commit')
- expect(page).to have_content('diff --git a/added_file.txt b/added_file.txt')
+ 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' do
@@ -32,7 +27,7 @@ module QA
@merge_request.visit!
Page::MergeRequest::Show.perform(&:view_plain_diff)
- expect(page.text).to start_with('diff --git a/added_file.txt b/added_file.txt')
+ expect(page.text).to start_with("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/repository/add_ssh_key_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb
index 474a7904fea..c3379d41ff2 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb
@@ -13,7 +13,7 @@ module QA
end
expect(page).to have_content("Title: #{key_title}")
- expect(page).to have_content(key.fingerprint)
+ expect(page).to have_content(key.md5_fingerprint)
Page::Main::Menu.perform(&:click_settings_link)
Page::Profile::Menu.perform(&:click_ssh_keys)
@@ -23,7 +23,7 @@ module QA
end
expect(page).not_to have_content("Title: #{key_title}")
- expect(page).not_to have_content(key.fingerprint)
+ expect(page).not_to have_content(key.md5_fingerprint)
end
end
end
diff --git a/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb b/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb
index 7c9db5ee496..70b571a316a 100644
--- a/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
- context 'Create' do
+ context 'Create', quarantine: 'https://gitlab.com/gitlab-org/gitlab/issues/196034' do
describe 'Web IDE file templates' do
include Runtime::Fixtures
diff --git a/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb b/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb
index 9c964c726f1..89aba112407 100644
--- a/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb
+++ b/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb
@@ -15,11 +15,11 @@ module QA
resource.key = deploy_key_value
end
- expect(deploy_key.fingerprint).to eq key.fingerprint
+ expect(deploy_key.md5_fingerprint).to eq key.md5_fingerprint
Page::Project::Settings::Repository.perform do |setting|
setting.expand_deploy_keys do |keys|
- expect(keys).to have_key(deploy_key_title, key.fingerprint)
+ expect(keys).to have_key(deploy_key_title, key.md5_fingerprint)
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 3badaa983cb..0ca49bd080b 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
@@ -3,13 +3,9 @@
require 'digest/sha1'
module QA
- context 'Release', :docker do
+ context 'Release', :docker, quarantine: 'https://gitlab.com/gitlab-org/gitlab/issues/196047' do
describe 'Git clone using a deploy key' do
before do
- # Handle WIP Job Logs flag - https://gitlab.com/gitlab-org/gitlab/issues/31162
- @job_log_json_flag_enabled = Runtime::Feature.enabled?('job_log_json')
- Runtime::Feature.disable('job_log_json') if @job_log_json_flag_enabled
-
Flow::Login.sign_in
@runner_name = "qa-runner-#{Time.now.to_i}"
@@ -29,7 +25,6 @@ module QA
end
after do
- Runtime::Feature.enable('job_log_json') if @job_log_json_flag_enabled
Service::DockerRun::GitlabRunner.new(@runner_name).remove!
end
diff --git a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb
index a96bfde49f3..54014ff7067 100644
--- a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb
+++ b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb
@@ -50,7 +50,8 @@ module QA
end
end
- describe 'Auto DevOps support', :orchestrated, :kubernetes do
+ # Failure issue: https://gitlab.com/gitlab-org/gitlab/issues/118481
+ describe 'Auto DevOps support', :orchestrated, :kubernetes, :quarantine do
context 'when rbac is enabled' do
before(:all) do
@cluster = Service::KubernetesCluster.new.create!
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
new file mode 100644
index 00000000000..73b5a579e08
--- /dev/null
+++ b/qa/qa/specs/features/browser_ui/7_configure/kubernetes/kubernetes_integration_spec.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+module QA
+ context 'Configure' do
+ # This test requires GITLAB_QA_ADMIN_ACCESS_TOKEN to be specified
+ describe 'Kubernetes Cluster Integration', :orchestrated, :kubernetes, :requires_admin, :skip do
+ context 'Project Clusters' do
+ let(:cluster) { Service::KubernetesCluster.new(provider_class: Service::ClusterProvider::K3d).create! }
+ let(:project) do
+ Resource::Project.fabricate_via_api! do |project|
+ project.name = 'project-with-k8s'
+ project.description = 'Project with Kubernetes cluster integration'
+ end
+ end
+
+ before do
+ Flow::Login.sign_in
+ end
+
+ after do
+ cluster.remove!
+ end
+
+ it 'can create and associate a project cluster', :smoke do
+ Resource::KubernetesCluster.fabricate_via_browser_ui! do |k8s_cluster|
+ k8s_cluster.project = project
+ k8s_cluster.cluster = cluster
+ end
+
+ project.visit!
+
+ Page::Project::Menu.perform(&:go_to_operations_kubernetes)
+
+ Page::Project::Operations::Kubernetes::Index.perform do |index|
+ expect(index).to have_cluster(cluster)
+ end
+ end
+
+ it 'installs helm and tiller on a gitlab managed app' do
+ Resource::KubernetesCluster.fabricate_via_browser_ui! do |k8s_cluster|
+ k8s_cluster.project = project
+ k8s_cluster.cluster = cluster
+ k8s_cluster.install_helm_tiller = true
+ end
+
+ Page::Project::Operations::Kubernetes::Show.perform do |show|
+ expect(show).to have_application_installed(:helm)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/browser_ui/non_devops/performance_bar_spec.rb b/qa/qa/specs/features/browser_ui/non_devops/performance_bar_spec.rb
index b067a44e325..4a5bb077e69 100644
--- a/qa/qa/specs/features/browser_ui/non_devops/performance_bar_spec.rb
+++ b/qa/qa/specs/features/browser_ui/non_devops/performance_bar_spec.rb
@@ -1,32 +1,36 @@
# frozen_string_literal: true
module QA
- # https://gitlab.com/gitlab-org/gitlab/issues/38315
- context 'Performance bar', :quarantine do
- context 'when logged in as an admin user', :requires_admin do
- before do
- Flow::Login.sign_in_as_admin
- Page::Main::Menu.perform(&:go_to_admin_area)
- Page::Admin::Menu.perform(&:go_to_metrics_and_profiling_settings)
+ context 'Non-devops' do
+ describe 'Performance bar display', :requires_admin do
+ context 'when logged in as an admin user' do
+ # 4 metrics: pg, gitaly, redis, total
+ let(:metrics_count) { 4 }
- Page::Admin::Settings::MetricsAndProfiling.perform do |setting|
- setting.expand_performance_bar do |page|
- page.enable_performance_bar
- page.save_settings
+ before do
+ Flow::Login.sign_in_as_admin
+ Page::Main::Menu.perform(&:go_to_admin_area)
+ Page::Admin::Menu.perform(&:go_to_metrics_and_profiling_settings)
+
+ Page::Admin::Settings::MetricsAndProfiling.perform do |setting|
+ setting.expand_performance_bar do |page|
+ page.enable_performance_bar
+ page.save_settings
+ end
end
end
- end
- it 'shows results for the original request and AJAX requests' do
- # Issue pages always make AJAX requests
- Resource::Issue.fabricate_via_browser_ui! do |issue|
- issue.title = 'Performance bar test'
- end
+ it 'shows results for the original request and AJAX requests' do
+ # Issue pages always make AJAX requests
+ Resource::Issue.fabricate_via_browser_ui! do |issue|
+ issue.title = 'Performance bar test'
+ end
- Page::Layout::PerformanceBar.perform do |bar_component|
- expect(bar_component).to have_performance_bar
- expect(bar_component).to have_detailed_metrics
- expect(bar_component).to have_request_for('realtime_changes') # Always requested on issue pages
+ Page::Layout::PerformanceBar.perform do |bar_component|
+ expect(bar_component).to have_performance_bar
+ expect(bar_component).to have_detailed_metrics(metrics_count)
+ expect(bar_component).to have_request_for('realtime_changes') # Always requested on issue pages
+ end
end
end
end
diff --git a/qa/qa/specs/helpers/quarantine.rb b/qa/qa/specs/helpers/quarantine.rb
index ca0ce32e74f..8b14184f3b7 100644
--- a/qa/qa/specs/helpers/quarantine.rb
+++ b/qa/qa/specs/helpers/quarantine.rb
@@ -43,7 +43,23 @@ module QA::Specs::Helpers
# 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
- skip('In quarantine') if example.metadata.key?(:quarantine)
+ if example.metadata.key?(:quarantine)
+ quarantine_message = %w(In quarantine)
+ quarantine_tag = example.metadata[:quarantine]
+
+ if !!quarantine_tag
+ quarantine_message << case quarantine_tag
+ when String
+ ": #{quarantine_tag}"
+ when Hash
+ ": #{quarantine_tag[:issue]}"
+ else
+ ''
+ end
+ end
+
+ skip(quarantine_message.join(' ').strip)
+ end
end
end
diff --git a/qa/qa/support/page/logging.rb b/qa/qa/support/page/logging.rb
index 5d73d9635f8..f59795e17c3 100644
--- a/qa/qa/support/page/logging.rb
+++ b/qa/qa/support/page/logging.rb
@@ -16,7 +16,7 @@ module QA
super
end
- def wait(max: 60, interval: 0.1, reload: true)
+ def wait_until(max_duration: 60, sleep_interval: 0.1, reload: true, raise_on_failure: false)
log("next wait uses reload: #{reload}")
# Logging of wait start/end/duration is handled by QA::Support::Waiter
@@ -119,10 +119,10 @@ module QA
found
end
- def has_no_text?(text)
+ def has_no_text?(text, **kwargs)
found = super
- log(%Q{has_no_text?('#{text}') returned #{found}})
+ log(%Q{has_no_text?('#{text}', wait: #{kwargs[:wait] || Capybara.default_max_wait_time}) returned #{found}})
found
end
@@ -173,6 +173,7 @@ module QA
def log_has_element_or_not(method, name, found, **kwargs)
msg = ["#{method} :#{name}"]
msg << %Q(with text "#{kwargs[:text]}") if kwargs[:text]
+ msg << "class: #{kwargs[:class]}" if kwargs[:class]
msg << "(wait: #{kwargs[:wait] || Capybara.default_max_wait_time})"
msg << "returned: #{found}"
diff --git a/qa/qa/support/repeater.rb b/qa/qa/support/repeater.rb
new file mode 100644
index 00000000000..53d72f2f410
--- /dev/null
+++ b/qa/qa/support/repeater.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+require 'active_support/inflector'
+
+module QA
+ module Support
+ module Repeater
+ DEFAULT_MAX_WAIT_TIME = 60
+
+ RetriesExceededError = Class.new(RuntimeError)
+ WaitExceededError = Class.new(RuntimeError)
+
+ def repeat_until(max_attempts: nil, max_duration: nil, reload_page: nil, sleep_interval: 0, raise_on_failure: true, retry_on_exception: false)
+ attempts = 0
+ start = Time.now
+
+ begin
+ while remaining_attempts?(attempts, max_attempts) && remaining_time?(start, max_duration)
+ QA::Runtime::Logger.debug("Attempt number #{attempts + 1}") if max_attempts
+
+ result = yield
+ return result if result
+
+ sleep_and_reload_if_needed(sleep_interval, reload_page)
+ attempts += 1
+ end
+ rescue StandardError, RSpec::Expectations::ExpectationNotMetError
+ 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)
+
+ retry
+ else
+ raise
+ end
+ end
+
+ if raise_on_failure
+ raise RetriesExceededError, "Retry condition not met after #{max_attempts} #{'attempt'.pluralize(max_attempts)}" unless remaining_attempts?(attempts, max_attempts)
+
+ raise WaitExceededError, "Wait condition not met after #{max_duration} #{'second'.pluralize(max_duration)}"
+ end
+
+ false
+ end
+
+ private
+
+ def sleep_and_reload_if_needed(sleep_interval, reload_page)
+ sleep(sleep_interval)
+ reload_page.refresh if reload_page
+ end
+
+ def remaining_attempts?(attempts, max_attempts)
+ max_attempts ? attempts < max_attempts : true
+ end
+
+ def remaining_time?(start, max_duration)
+ max_duration ? Time.now - start < max_duration : true
+ end
+ end
+ end
+end
diff --git a/qa/qa/support/retrier.rb b/qa/qa/support/retrier.rb
index 3b02cb4855b..7b548e95453 100644
--- a/qa/qa/support/retrier.rb
+++ b/qa/qa/support/retrier.rb
@@ -3,49 +3,61 @@
module QA
module Support
module Retrier
+ extend Repeater
+
module_function
def retry_on_exception(max_attempts: 3, reload_page: nil, sleep_interval: 0.5)
- QA::Runtime::Logger.debug("with retry_on_exception: max_attempts #{max_attempts}; sleep_interval #{sleep_interval}")
-
- attempts = 0
+ QA::Runtime::Logger.debug(
+ <<~MSG.tr("\n", ' ')
+ with retry_on_exception: max_attempts: #{max_attempts};
+ reload_page: #{reload_page};
+ sleep_interval: #{sleep_interval}
+ MSG
+ )
- begin
- QA::Runtime::Logger.debug("Attempt number #{attempts + 1}")
- yield
- rescue StandardError, RSpec::Expectations::ExpectationNotMetError
- sleep sleep_interval
- reload_page.refresh if reload_page
- attempts += 1
+ result = nil
+ repeat_until(
+ max_attempts: max_attempts,
+ reload_page: reload_page,
+ sleep_interval: sleep_interval,
+ retry_on_exception: true
+ ) do
+ result = yield
- retry if attempts < max_attempts
- QA::Runtime::Logger.debug("Raising exception after #{max_attempts} attempts")
- raise
+ # This method doesn't care what the return value of the block is.
+ # We set it to `true` so that it doesn't repeat if there's no exception
+ true
end
- end
-
- def retry_until(max_attempts: 3, reload_page: nil, sleep_interval: 0, exit_on_failure: false)
- QA::Runtime::Logger.debug("with retry_until: max_attempts #{max_attempts}; sleep_interval #{sleep_interval}; reload_page:#{reload_page}")
- attempts = 0
+ QA::Runtime::Logger.debug("ended retry_on_exception")
- while attempts < max_attempts
- QA::Runtime::Logger.debug("Attempt number #{attempts + 1}")
- result = yield
- return result if result
+ result
+ end
- sleep sleep_interval
+ def retry_until(max_attempts: nil, max_duration: nil, reload_page: nil, sleep_interval: 0, raise_on_failure: false, retry_on_exception: false)
+ # For backwards-compatibility
+ max_attempts = 3 if max_attempts.nil? && max_duration.nil?
- reload_page.refresh if reload_page
+ start_msg ||= ["with retry_until:"]
+ start_msg << "max_attempts: #{max_attempts};" if max_attempts
+ start_msg << "max_duration: #{max_duration};" if max_duration
+ start_msg << "reload_page: #{reload_page}; sleep_interval: #{sleep_interval}; raise_on_failure: #{raise_on_failure}; retry_on_exception: #{retry_on_exception}"
+ QA::Runtime::Logger.debug(start_msg.join(' '))
- attempts += 1
- end
-
- if exit_on_failure
- QA::Runtime::Logger.debug("Raising exception after #{max_attempts} attempts")
- raise
+ result = nil
+ repeat_until(
+ max_attempts: max_attempts,
+ max_duration: max_duration,
+ reload_page: reload_page,
+ sleep_interval: sleep_interval,
+ raise_on_failure: raise_on_failure,
+ retry_on_exception: retry_on_exception
+ ) do
+ result = yield
end
+ QA::Runtime::Logger.debug("ended retry_until")
- false
+ result
end
end
end
diff --git a/qa/qa/support/wait_for_requests.rb b/qa/qa/support/wait_for_requests.rb
new file mode 100644
index 00000000000..5d5ba70a0c2
--- /dev/null
+++ b/qa/qa/support/wait_for_requests.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module QA
+ module Support
+ module WaitForRequests
+ module_function
+
+ def wait_for_requests
+ Waiter.wait_until do
+ finished_all_ajax_requests? && finished_all_axios_requests?
+ end
+ end
+
+ def finished_all_axios_requests?
+ Capybara.page.evaluate_script('window.pendingRequests || 0').zero?
+ end
+
+ def finished_all_ajax_requests?
+ return true if Capybara.page.evaluate_script('typeof jQuery === "undefined"')
+
+ Capybara.page.evaluate_script('jQuery.active').zero?
+ end
+ end
+ end
+end
diff --git a/qa/qa/support/waiter.rb b/qa/qa/support/waiter.rb
index fdcf2d7e157..fe63c930c7c 100644
--- a/qa/qa/support/waiter.rb
+++ b/qa/qa/support/waiter.rb
@@ -3,30 +3,33 @@
module QA
module Support
module Waiter
- DEFAULT_MAX_WAIT_TIME = 60
+ extend Repeater
module_function
- def wait(max: DEFAULT_MAX_WAIT_TIME, interval: 0.1)
- QA::Runtime::Logger.debug("with wait: max #{max}; interval #{interval}")
- start = Time.now
+ def wait_until(max_duration: singleton_class::DEFAULT_MAX_WAIT_TIME, reload_page: nil, sleep_interval: 0.1, raise_on_failure: false, retry_on_exception: false)
+ QA::Runtime::Logger.debug(
+ <<~MSG.tr("\n", ' ')
+ with wait_until: max_duration: #{max_duration};
+ reload_page: #{reload_page};
+ sleep_interval: #{sleep_interval};
+ raise_on_failure: #{raise_on_failure}
+ MSG
+ )
- while Time.now - start < max
+ result = nil
+ self.repeat_until(
+ max_duration: max_duration,
+ reload_page: reload_page,
+ sleep_interval: sleep_interval,
+ raise_on_failure: raise_on_failure,
+ retry_on_exception: retry_on_exception
+ ) do
result = yield
- if result
- log_end(Time.now - start)
- return result
- end
-
- sleep(interval)
end
- log_end(Time.now - start)
-
- false
- end
+ QA::Runtime::Logger.debug("ended wait_until")
- def self.log_end(duration)
- QA::Runtime::Logger.debug("ended wait after #{duration} seconds")
+ result
end
end
end
diff --git a/qa/qa/vendor/github/page/login.rb b/qa/qa/vendor/github/page/login.rb
index e581edcb7c7..4675e33b514 100644
--- a/qa/qa/vendor/github/page/login.rb
+++ b/qa/qa/vendor/github/page/login.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
require 'capybara/dsl'
+require 'benchmark'
module QA
module Vendor
@@ -12,10 +13,16 @@ module QA
fill_in 'password', with: QA::Runtime::Env.github_password
click_on 'Sign in'
- Support::Retrier.retry_until(exit_on_failure: true, sleep_interval: 35) do
- otp = OnePassword::CLI.new.otp
+ Support::Retrier.retry_until(raise_on_failure: true, sleep_interval: 35) do
+ fresh_otp = nil
- fill_in 'otp', with: otp
+ time = Benchmark.realtime do
+ fresh_otp = OnePassword::CLI.instance.fresh_otp
+ end
+
+ QA::Runtime::Logger.info("Returned fresh_otp: #{fresh_otp} in #{time} seconds")
+
+ fill_in 'otp', with: fresh_otp
click_on 'Verify'
diff --git a/qa/qa/vendor/jenkins/page/configure.rb b/qa/qa/vendor/jenkins/page/configure.rb
index 8851a2564fd..da59060152d 100644
--- a/qa/qa/vendor/jenkins/page/configure.rb
+++ b/qa/qa/vendor/jenkins/page/configure.rb
@@ -18,7 +18,7 @@ module QA
dropdown_element = find('.setting-name', text: "Credentials").find(:xpath, "..").find('select')
- QA::Support::Retrier.retry_until(exit_on_failure: true) do
+ QA::Support::Retrier.retry_until(raise_on_failure: true) do
dropdown_element.select "GitLab API token (#{token_description})"
dropdown_element.value != ''
end
diff --git a/qa/qa/vendor/jenkins/page/login.rb b/qa/qa/vendor/jenkins/page/login.rb
index 7b3558b25e2..b18c02b5a44 100644
--- a/qa/qa/vendor/jenkins/page/login.rb
+++ b/qa/qa/vendor/jenkins/page/login.rb
@@ -14,7 +14,7 @@ module QA
def visit!
super
- QA::Support::Retrier.retry_until(sleep_interval: 3, reload_page: page, max_attempts: 20, exit_on_failure: true) do
+ QA::Support::Retrier.retry_until(sleep_interval: 3, reload_page: page, max_attempts: 20, raise_on_failure: true) do
page.has_text? 'Welcome to Jenkins!'
end
end
diff --git a/qa/qa/vendor/jenkins/page/new_credentials.rb b/qa/qa/vendor/jenkins/page/new_credentials.rb
index bdef1a13fd4..b0d13973090 100644
--- a/qa/qa/vendor/jenkins/page/new_credentials.rb
+++ b/qa/qa/vendor/jenkins/page/new_credentials.rb
@@ -39,7 +39,7 @@ module QA
end
def wait_for_page_to_load
- QA::Support::Waiter.wait(interval: 1.0) do
+ QA::Support::Waiter.wait_until(sleep_interval: 1.0) do
page.has_css?('.setting-name', text: "Description")
end
end
diff --git a/qa/qa/vendor/one_password/cli.rb b/qa/qa/vendor/one_password/cli.rb
index 3cb69391783..cf8b7f8a4f9 100644
--- a/qa/qa/vendor/one_password/cli.rb
+++ b/qa/qa/vendor/one_password/cli.rb
@@ -1,9 +1,13 @@
# frozen_string_literal: true
+require 'benchmark'
+
module QA
module Vendor
module OnePassword
class CLI
+ include Singleton
+
def initialize
@email = QA::Runtime::Env.gitlab_qa_1p_email
@password = QA::Runtime::Env.gitlab_qa_1p_password
@@ -11,14 +15,39 @@ module QA
@github_uuid = QA::Runtime::Env.gitlab_qa_1p_github_uuid
end
- def otp
- `#{op_path} get totp #{@github_uuid} --session=#{session_token}`.to_i
+ def fresh_otp
+ otps = []
+
+ # Fetches a fresh OTP and returns it only after op provides the same OTP twice
+ # An OTP is valid for 30 seconds so 70 attempts with 0.5 interval would ensure we complete 1 cycle
+ Support::Retrier.retry_until(max_attempts: 70, sleep_interval: 0.5) do
+ otps << fetch_otp
+ otps.size >= 3 && otps[-1] == otps[-2] && otps[-1] != otps[-3]
+ end
+
+ otps.last
end
private
+ def fetch_otp
+ result = nil
+
+ time = Benchmark.realtime do
+ result = `#{op_path} get totp #{@github_uuid} --session=#{session_token}`.to_i
+ end
+
+ QA::Runtime::Logger.info("Fetched OTP: #{result} in: #{time} seconds")
+
+ result
+ end
+
+ # OP session tokens are valid for 30 minutes. We are caching the session token here and this is fine currently
+ # as we just have one test that is not expected to go over 30 minutes.
+ # But note that if we add more tests that use this class, we might need to add a mechanism to invalidate
+ # the cache after 30 minutes or if the session_token is rejected by op CLI.
def session_token
- `echo '#{@password}' | #{op_path} signin gitlab.1password.com #{@email} #{@secret} --output=raw --shorthand=gitlab_qa`
+ @session_token ||= `echo '#{@password}' | #{op_path} signin gitlab.1password.com #{@email} #{@secret} --output=raw --shorthand=gitlab_qa`
end
def op_path
diff --git a/qa/qa/vendor/one_password/darwin/op b/qa/qa/vendor/one_password/darwin/op
index 0f646522834..be7a3721b14 100755
--- a/qa/qa/vendor/one_password/darwin/op
+++ b/qa/qa/vendor/one_password/darwin/op
Binary files differ
diff --git a/qa/qa/vendor/one_password/linux/op b/qa/qa/vendor/one_password/linux/op
index 47ce87731be..47e79d7c599 100755
--- a/qa/qa/vendor/one_password/linux/op
+++ b/qa/qa/vendor/one_password/linux/op
Binary files differ
diff --git a/qa/spec/page/base_spec.rb b/qa/spec/page/base_spec.rb
index 9e3f143ea5b..2d13889d26d 100644
--- a/qa/spec/page/base_spec.rb
+++ b/qa/spec/page/base_spec.rb
@@ -3,7 +3,7 @@
describe QA::Page::Base do
describe 'page helpers' do
it 'exposes helpful page helpers' do
- expect(subject).to respond_to :refresh, :wait, :scroll_to
+ expect(subject).to respond_to :refresh, :wait_until, :scroll_to
end
end
@@ -62,18 +62,18 @@ describe QA::Page::Base do
end
end
- describe '#wait' do
+ describe '#wait_until' do
subject { Class.new(described_class).new }
context 'when the condition is true' do
it 'does not refresh' do
expect(subject).not_to receive(:refresh)
- subject.wait(max: 0.01) { true }
+ subject.wait_until(max_duration: 0.01, raise_on_failure: false) { true }
end
it 'returns true' do
- expect(subject.wait(max: 0.1) { true }).to be_truthy
+ expect(subject.wait_until(max_duration: 0.1, raise_on_failure: false) { true }).to be_truthy
end
end
@@ -81,13 +81,29 @@ describe QA::Page::Base do
it 'refreshes' do
expect(subject).to receive(:refresh).at_least(:once)
- subject.wait(max: 0.01) { false }
+ subject.wait_until(max_duration: 0.01, raise_on_failure: false) { false }
end
it 'returns false' do
allow(subject).to receive(:refresh)
- expect(subject.wait(max: 0.01) { false }).to be_falsey
+ expect(subject.wait_until(max_duration: 0.01, raise_on_failure: false) { false }).to be_falsey
+ end
+ end
+ end
+
+ describe '#all_elements' do
+ before do
+ allow(subject).to receive(:all)
+ end
+
+ it 'raises an error if count or minimum are not specified' do
+ expect { subject.all_elements(:foo) }.to raise_error ArgumentError
+ end
+
+ it 'does not raise an error if :minimum, :maximum, :count, or :between is specified' do
+ [:minimum, :maximum, :count, :between].each do |param|
+ expect { subject.all_elements(:foo, param => 1) }.not_to raise_error
end
end
end
diff --git a/qa/spec/page/logging_spec.rb b/qa/spec/page/logging_spec.rb
index fb89bcd3ab4..a6b61e9b1ee 100644
--- a/qa/spec/page/logging_spec.rb
+++ b/qa/spec/page/logging_spec.rb
@@ -28,21 +28,21 @@ describe QA::Support::Page::Logging do
end
it 'logs wait' do
- expect { subject.wait(max: 0) {} }
+ expect { subject.wait_until(max_duration: 0) {} }
.to output(/next wait uses reload: true/).to_stdout_from_any_process
- expect { subject.wait(max: 0) {} }
- .to output(/with wait/).to_stdout_from_any_process
- expect { subject.wait(max: 0) {} }
- .to output(/ended wait after .* seconds$/).to_stdout_from_any_process
+ expect { subject.wait_until(max_duration: 0) {} }
+ .to output(/with wait_until/).to_stdout_from_any_process
+ expect { subject.wait_until(max_duration: 0) {} }
+ .to output(/ended wait_until$/).to_stdout_from_any_process
end
it 'logs wait with reload false' do
- expect { subject.wait(max: 0, reload: false) {} }
+ expect { subject.wait_until(max_duration: 0, reload: false) {} }
.to output(/next wait uses reload: false/).to_stdout_from_any_process
- expect { subject.wait(max: 0, reload: false) {} }
- .to output(/with wait/).to_stdout_from_any_process
- expect { subject.wait(max: 0, reload: false) {} }
- .to output(/ended wait after .* seconds$/).to_stdout_from_any_process
+ expect { subject.wait_until(max_duration: 0, reload: false) {} }
+ .to output(/with wait_until/).to_stdout_from_any_process
+ expect { subject.wait_until(max_duration: 0, reload: false) {} }
+ .to output(/ended wait_until$/).to_stdout_from_any_process
end
it 'logs scroll_to' do
@@ -121,10 +121,10 @@ describe QA::Support::Page::Logging do
end
it 'logs has_no_text?' do
- allow(page).to receive(:has_no_text?).with('foo').and_return(true)
+ allow(page).to receive(:has_no_text?).with('foo', any_args).and_return(true)
expect { subject.has_no_text? 'foo' }
- .to output(/has_no_text\?\('foo'\) returned true/).to_stdout_from_any_process
+ .to output(/has_no_text\?\('foo', wait: #{QA::Runtime::Browser::CAPYBARA_MAX_WAIT_TIME}\) returned true/).to_stdout_from_any_process
end
it 'logs finished_loading?' do
@@ -145,18 +145,18 @@ describe QA::Support::Page::Logging do
it 'logs the number of elements found' do
allow(page).to receive(:all).and_return([1, 2])
- expect { subject.all_elements(:element) }
+ expect { subject.all_elements(:element, count: 2) }
.to output(/finding all :element/).to_stdout_from_any_process
- expect { subject.all_elements(:element) }
+ expect { subject.all_elements(:element, count: 2) }
.to output(/found 2 :element/).to_stdout_from_any_process
end
it 'logs 0 if no elements are found' do
allow(page).to receive(:all).and_return([])
- expect { subject.all_elements(:element) }
+ expect { subject.all_elements(:element, count: 1) }
.to output(/finding all :element/).to_stdout_from_any_process
- expect { subject.all_elements(:element) }
+ expect { subject.all_elements(:element, count: 1) }
.not_to output(/found 0 :elements/).to_stdout_from_any_process
end
end
diff --git a/qa/spec/resource/events/project_spec.rb b/qa/spec/resource/events/project_spec.rb
index b3efdb518f3..dd544ec7ac8 100644
--- a/qa/spec/resource/events/project_spec.rb
+++ b/qa/spec/resource/events/project_spec.rb
@@ -33,6 +33,7 @@ describe QA::Resource::Events::Project do
before do
allow(subject).to receive(:max_wait).and_return(0.01)
+ allow(subject).to receive(:raise_on_failure).and_return(false)
allow(subject).to receive(:parse_body).and_return(all_events)
end
diff --git a/qa/spec/runtime/application_settings_spec.rb b/qa/spec/runtime/application_settings_spec.rb
new file mode 100644
index 00000000000..fce0361aee0
--- /dev/null
+++ b/qa/spec/runtime/application_settings_spec.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+describe QA::Runtime::ApplicationSettings do
+ let(:api_client) { double('QA::Runtime::API::Client') }
+ let(:request) { Struct.new(:url).new('http://api') }
+ let(:get_response) { Struct.new(:body).new("{}") }
+
+ before do
+ allow(described_class).to receive(:api_client).and_return(api_client)
+ end
+
+ describe '.set_application_settings' do
+ it 'sets application settings' do
+ expect(QA::Runtime::API::Request)
+ .to receive(:new)
+ .with(api_client, '/application/settings')
+ .and_return(request)
+
+ expect(described_class)
+ .to receive(:put)
+ .with(request.url, { allow_local_requests_from_web_hooks_and_services: true })
+ .and_return(Struct.new(:code).new(200))
+
+ subject.set_application_settings(allow_local_requests_from_web_hooks_and_services: true)
+ end
+ end
+
+ describe '.get_application_settings' do
+ it 'gets application settings' do
+ expect(QA::Runtime::API::Request)
+ .to receive(:new)
+ .with(api_client, '/application/settings')
+ .and_return(request)
+
+ expect(described_class)
+ .to receive(:get)
+ .with(request.url)
+ .and_return(get_response)
+
+ subject.get_application_settings
+ end
+ end
+end
diff --git a/qa/spec/runtime/env_spec.rb b/qa/spec/runtime/env_spec.rb
index 340831aa06d..0a0bf33a726 100644
--- a/qa/spec/runtime/env_spec.rb
+++ b/qa/spec/runtime/env_spec.rb
@@ -230,6 +230,20 @@ describe QA::Runtime::Env do
end
end
+ describe '.require_admin_access_token!' do
+ it 'raises ArgumentError if GITLAB_QA_ADMIN_ACCESS_TOKEN is not specified' do
+ stub_env('GITLAB_QA_ADMIN_ACCESS_TOKEN', nil)
+
+ expect { described_class.require_admin_access_token! }.to raise_error(ArgumentError)
+ end
+
+ it 'does not raise exception if GITLAB_QA_ADMIN_ACCESS_TOKEN is specified' do
+ stub_env('GITLAB_QA_ADMIN_ACCESS_TOKEN', 'foobar123')
+
+ expect { described_class.require_admin_access_token! }.not_to raise_error
+ end
+ end
+
describe '.log_destination' do
it 'returns $stdout if QA_LOG_PATH is not defined' do
stub_env('QA_LOG_PATH', nil)
diff --git a/qa/spec/spec_helper.rb b/qa/spec/spec_helper.rb
index 3a26ed89e9c..1336bea16bc 100644
--- a/qa/spec/spec_helper.rb
+++ b/qa/spec/spec_helper.rb
@@ -12,9 +12,9 @@ QA::Runtime::Browser.configure!
QA::Runtime::Scenario.from_env(QA::Runtime::Env.runtime_scenario_attributes) if QA::Runtime::Env.runtime_scenario_attributes
-%w[helpers shared_examples].each do |d|
- Dir[::File.join(__dir__, d, '**', '*.rb')].each { |f| require f }
-end
+Dir[::File.join(__dir__, "support/helpers/*.rb")].each { |f| require f }
+Dir[::File.join(__dir__, "support/shared_contexts/*.rb")].each { |f| require f }
+Dir[::File.join(__dir__, "support/shared_examples/*.rb")].each { |f| require f }
RSpec.configure do |config|
QA::Specs::Helpers::Quarantine.configure_rspec
diff --git a/qa/spec/specs/helpers/quarantine_spec.rb b/qa/spec/specs/helpers/quarantine_spec.rb
index 2538632c032..d5c6820f0a9 100644
--- a/qa/spec/specs/helpers/quarantine_spec.rb
+++ b/qa/spec/specs/helpers/quarantine_spec.rb
@@ -155,6 +155,26 @@ describe QA::Specs::Helpers::Quarantine do
expect(group.examples.first.execution_result.status).to eq(:passed)
end
+
+ context 'quarantine message' do
+ shared_examples 'test with quarantine message' do |quarantine_tag|
+ it 'outputs the quarantine message' do
+ group = describe_successfully do
+ it('is quarantined', quarantine: quarantine_tag) {}
+ end
+
+ expect(group.examples.first.execution_result.pending_message)
+ .to eq('In quarantine : for a reason')
+ end
+ end
+
+ it_behaves_like 'test with quarantine message', 'for a reason'
+
+ it_behaves_like 'test with quarantine message', {
+ issue: 'for a reason',
+ environment: [:nightly, :staging]
+ }
+ end
end
context 'with :quarantine focused' do
diff --git a/qa/spec/helpers/stub_env.rb b/qa/spec/support/helpers/stub_env.rb
index 8ad864dbec8..8ad864dbec8 100644
--- a/qa/spec/helpers/stub_env.rb
+++ b/qa/spec/support/helpers/stub_env.rb
diff --git a/qa/spec/support/repeater_spec.rb b/qa/spec/support/repeater_spec.rb
new file mode 100644
index 00000000000..20dca6608f6
--- /dev/null
+++ b/qa/spec/support/repeater_spec.rb
@@ -0,0 +1,385 @@
+# frozen_string_literal: true
+
+require 'logger'
+require 'timecop'
+require 'active_support/core_ext/integer/time'
+
+describe QA::Support::Repeater do
+ before do
+ logger = ::Logger.new $stdout
+ logger.level = ::Logger::DEBUG
+ QA::Runtime::Logger.logger = logger
+ end
+
+ subject do
+ Module.new do
+ extend QA::Support::Repeater
+ end
+ end
+
+ let(:time_start) { Time.now }
+ let(:return_value) { "test passed" }
+
+ describe '.repeat_until' do
+ context 'when raise_on_failure is not provided (default: true)' do
+ context 'when retry_on_exception is not provided (default: false)' do
+ context 'when max_duration is provided' do
+ context 'when max duration is reached' do
+ it 'raises an exception' do
+ expect do
+ Timecop.freeze do
+ subject.repeat_until(max_duration: 1) do
+ Timecop.travel(2)
+ false
+ end
+ end
+ end.to raise_error(QA::Support::Repeater::WaitExceededError, "Wait condition not met after 1 second")
+ end
+
+ it 'ignores attempts' do
+ loop_counter = 0
+
+ expect(
+ Timecop.freeze do
+ subject.repeat_until(max_duration: 1) do
+ loop_counter += 1
+
+ if loop_counter > 3
+ Timecop.travel(1)
+ return_value
+ else
+ false
+ end
+ end
+ end
+ ).to eq(return_value)
+ expect(loop_counter).to eq(4)
+ end
+ end
+
+ context 'when max duration is not reached' do
+ it 'returns value from block' do
+ Timecop.freeze(time_start) do
+ expect(
+ subject.repeat_until(max_duration: 1) do
+ return_value
+ end
+ ).to eq(return_value)
+ end
+ end
+ end
+ end
+
+ context 'when max_attempts is provided' do
+ context 'when max_attempts is reached' do
+ it 'raises an exception' do
+ expect do
+ Timecop.freeze do
+ subject.repeat_until(max_attempts: 1) do
+ false
+ end
+ end
+ end.to raise_error(QA::Support::Repeater::RetriesExceededError, "Retry condition not met after 1 attempt")
+ end
+
+ it 'ignores duration' do
+ loop_counter = 0
+
+ expect(
+ Timecop.freeze do
+ subject.repeat_until(max_attempts: 2) do
+ loop_counter += 1
+ Timecop.travel(1.year)
+
+ if loop_counter > 1
+ return_value
+ else
+ false
+ end
+ end
+ end
+ ).to eq(return_value)
+ expect(loop_counter).to eq(2)
+ end
+ end
+
+ context 'when max_attempts is not reached' do
+ it 'returns value from block' do
+ expect(
+ Timecop.freeze do
+ subject.repeat_until(max_attempts: 1) do
+ return_value
+ end
+ end
+ ).to eq(return_value)
+ end
+ end
+ end
+
+ context 'when both max_attempts and max_duration are provided' do
+ context 'when max_attempts is reached first' do
+ it 'raises an exception' do
+ loop_counter = 0
+ expect do
+ Timecop.freeze do
+ subject.repeat_until(max_attempts: 1, max_duration: 2) do
+ loop_counter += 1
+ Timecop.travel(time_start + loop_counter)
+ false
+ end
+ end
+ end.to raise_error(QA::Support::Repeater::RetriesExceededError, "Retry condition not met after 1 attempt")
+ end
+ end
+
+ context 'when max_duration is reached first' do
+ it 'raises an exception' do
+ loop_counter = 0
+ expect do
+ Timecop.freeze do
+ subject.repeat_until(max_attempts: 2, max_duration: 1) do
+ loop_counter += 1
+ Timecop.travel(time_start + loop_counter)
+ false
+ end
+ end
+ end.to raise_error(QA::Support::Repeater::WaitExceededError, "Wait condition not met after 1 second")
+ end
+ end
+ end
+ end
+
+ context 'when retry_on_exception is true' do
+ context 'when max duration is reached' do
+ it 'raises an exception' do
+ Timecop.freeze do
+ expect do
+ subject.repeat_until(max_duration: 1, retry_on_exception: true) do
+ Timecop.travel(2)
+
+ raise "this should be raised"
+ end
+ end.to raise_error(RuntimeError, "this should be raised")
+ end
+ end
+
+ it 'does not raise an exception until max_duration is reached' do
+ loop_counter = 0
+
+ Timecop.freeze(time_start) do
+ expect do
+ subject.repeat_until(max_duration: 2, retry_on_exception: true) do
+ loop_counter += 1
+ Timecop.travel(time_start + loop_counter)
+
+ raise "this should be raised"
+ end
+ end.to raise_error(RuntimeError, "this should be raised")
+ end
+ expect(loop_counter).to eq(2)
+ end
+ end
+
+ context 'when max duration is not reached' do
+ it 'returns value from block' do
+ loop_counter = 0
+
+ Timecop.freeze(time_start) do
+ expect(
+ subject.repeat_until(max_duration: 3, retry_on_exception: true) do
+ loop_counter += 1
+ Timecop.travel(time_start + loop_counter)
+
+ raise "this should not be raised" if loop_counter == 1
+
+ return_value
+ end
+ ).to eq(return_value)
+ end
+ expect(loop_counter).to eq(2)
+ end
+ end
+
+ context 'when both max_attempts and max_duration are provided' do
+ context 'when max_attempts is reached first' do
+ it 'raises an exception' do
+ loop_counter = 0
+ expect do
+ Timecop.freeze do
+ subject.repeat_until(max_attempts: 1, max_duration: 2, retry_on_exception: true) do
+ loop_counter += 1
+ Timecop.travel(time_start + loop_counter)
+ false
+ end
+ end
+ end.to raise_error(QA::Support::Repeater::RetriesExceededError, "Retry condition not met after 1 attempt")
+ end
+ end
+
+ context 'when max_duration is reached first' do
+ it 'raises an exception' do
+ loop_counter = 0
+ expect do
+ Timecop.freeze do
+ subject.repeat_until(max_attempts: 2, max_duration: 1, retry_on_exception: true) do
+ loop_counter += 1
+ Timecop.travel(time_start + loop_counter)
+ false
+ end
+ end
+ end.to raise_error(QA::Support::Repeater::WaitExceededError, "Wait condition not met after 1 second")
+ end
+ end
+ end
+ end
+ end
+
+ context 'when raise_on_failure is false' do
+ context 'when retry_on_exception is not provided (default: false)' do
+ context 'when max duration is reached' do
+ def test_wait
+ Timecop.freeze do
+ subject.repeat_until(max_duration: 1, raise_on_failure: false) do
+ Timecop.travel(2)
+ return_value
+ end
+ end
+ end
+
+ it 'does not raise an exception' do
+ expect { test_wait }.not_to raise_error
+ end
+
+ it 'returns the value from the block' do
+ expect(test_wait).to eq(return_value)
+ end
+ end
+
+ context 'when max duration is not reached' do
+ it 'returns the value from the block' do
+ Timecop.freeze do
+ expect(
+ subject.repeat_until(max_duration: 1, raise_on_failure: false) do
+ return_value
+ end
+ ).to eq(return_value)
+ end
+ end
+
+ it 'raises an exception' do
+ Timecop.freeze do
+ expect do
+ subject.repeat_until(max_duration: 1, raise_on_failure: false) do
+ raise "this should be raised"
+ end
+ end.to raise_error(RuntimeError, "this should be raised")
+ end
+ end
+ end
+
+ context 'when both max_attempts and max_duration are provided' do
+ shared_examples 'repeat until' do |max_attempts:, max_duration:|
+ it "returns when #{max_attempts < max_duration ? 'max_attempts' : 'max_duration'} is reached" do
+ loop_counter = 0
+
+ expect(
+ Timecop.freeze do
+ subject.repeat_until(max_attempts: max_attempts, max_duration: max_duration, raise_on_failure: false) do
+ loop_counter += 1
+ Timecop.travel(time_start + loop_counter)
+ false
+ end
+ end
+ ).to eq(false)
+ expect(loop_counter).to eq(1)
+ end
+ end
+
+ context 'when max_attempts is reached first' do
+ it_behaves_like 'repeat until', max_attempts: 1, max_duration: 2
+ end
+
+ context 'when max_duration is reached first' do
+ it_behaves_like 'repeat until', max_attempts: 2, max_duration: 1
+ end
+ end
+ end
+
+ context 'when retry_on_exception is true' do
+ context 'when max duration is reached' do
+ def test_wait
+ Timecop.freeze do
+ subject.repeat_until(max_duration: 1, raise_on_failure: false, retry_on_exception: true) do
+ Timecop.travel(2)
+ return_value
+ end
+ end
+ end
+
+ it 'does not raise an exception' do
+ expect { test_wait }.not_to raise_error
+ end
+
+ it 'returns the value from the block' do
+ expect(test_wait).to eq(return_value)
+ end
+ end
+
+ context 'when max duration is not reached' do
+ before do
+ @loop_counter = 0
+ end
+
+ def test_wait_with_counter
+ Timecop.freeze(time_start) do
+ subject.repeat_until(max_duration: 3, raise_on_failure: false, retry_on_exception: true) do
+ @loop_counter += 1
+ Timecop.travel(time_start + @loop_counter)
+
+ raise "this should not be raised" if @loop_counter == 1
+
+ return_value
+ end
+ end
+ end
+
+ it 'does not raise an exception' do
+ expect { test_wait_with_counter }.not_to raise_error
+ end
+
+ it 'returns the value from the block' do
+ expect(test_wait_with_counter).to eq(return_value)
+ expect(@loop_counter).to eq(2)
+ end
+ end
+
+ context 'when both max_attempts and max_duration are provided' do
+ shared_examples 'repeat until' do |max_attempts:, max_duration:|
+ it "returns when #{max_attempts < max_duration ? 'max_attempts' : 'max_duration'} is reached" do
+ loop_counter = 0
+
+ expect(
+ Timecop.freeze do
+ subject.repeat_until(max_attempts: max_attempts, max_duration: max_duration, raise_on_failure: false, retry_on_exception: true) do
+ loop_counter += 1
+ Timecop.travel(time_start + loop_counter)
+ false
+ end
+ end
+ ).to eq(false)
+ expect(loop_counter).to eq(1)
+ end
+ end
+
+ context 'when max_attempts is reached first' do
+ it_behaves_like 'repeat until', max_attempts: 1, max_duration: 2
+ end
+
+ context 'when max_duration is reached first' do
+ it_behaves_like 'repeat until', max_attempts: 2, max_duration: 1
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/spec/support/retrier_spec.rb b/qa/spec/support/retrier_spec.rb
new file mode 100644
index 00000000000..fbe66a680f9
--- /dev/null
+++ b/qa/spec/support/retrier_spec.rb
@@ -0,0 +1,126 @@
+# frozen_string_literal: true
+
+require 'logger'
+require 'timecop'
+
+describe QA::Support::Retrier do
+ before do
+ logger = ::Logger.new $stdout
+ logger.level = ::Logger::DEBUG
+ QA::Runtime::Logger.logger = logger
+ end
+
+ describe '.retry_until' do
+ context 'when the condition is true' do
+ it 'logs max attempts (3 by default)' do
+ expect { subject.retry_until { true } }
+ .to output(/with retry_until: max_attempts: 3; reload_page: ; sleep_interval: 0; raise_on_failure: false; retry_on_exception: false/).to_stdout_from_any_process
+ end
+
+ it 'logs max duration' do
+ expect { subject.retry_until(max_duration: 1) { true } }
+ .to output(/with retry_until: max_duration: 1; reload_page: ; sleep_interval: 0; raise_on_failure: false; retry_on_exception: false/).to_stdout_from_any_process
+ end
+
+ it 'logs the end' do
+ expect { subject.retry_until { true } }
+ .to output(/ended retry_until$/).to_stdout_from_any_process
+ end
+ end
+
+ context 'when the condition is false' do
+ it 'logs the start' do
+ expect { subject.retry_until(max_duration: 0) { false } }
+ .to output(/with retry_until: max_duration: 0; reload_page: ; sleep_interval: 0; raise_on_failure: false; retry_on_exception: false/).to_stdout_from_any_process
+ end
+
+ it 'logs the end' do
+ expect { subject.retry_until(max_duration: 0) { false } }
+ .to output(/ended retry_until$/).to_stdout_from_any_process
+ end
+ end
+
+ context 'when max_duration and max_attempts are nil' do
+ it 'sets max attempts to 3 by default' do
+ expect(subject).to receive(:repeat_until).with(hash_including(max_attempts: 3))
+
+ subject.retry_until
+ end
+ end
+
+ it 'sets sleep_interval to 0 by default' do
+ expect(subject).to receive(:repeat_until).with(hash_including(sleep_interval: 0))
+
+ subject.retry_until
+ end
+
+ it 'sets raise_on_failure to false by default' do
+ expect(subject).to receive(:repeat_until).with(hash_including(raise_on_failure: false))
+
+ subject.retry_until
+ end
+
+ it 'sets retry_on_exception to false by default' do
+ expect(subject).to receive(:repeat_until).with(hash_including(retry_on_exception: false))
+
+ subject.retry_until
+ end
+ end
+
+ 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
+ end
+
+ it 'logs the end' do
+ expect { subject.retry_on_exception(max_attempts: 1, reload_page: nil, sleep_interval: 0) { true } }
+ .to output(/ended retry_on_exception$/).to_stdout_from_any_process
+ end
+ end
+
+ 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
+ end
+
+ it 'logs the end' do
+ expect { subject.retry_on_exception(max_attempts: 1, reload_page: nil, sleep_interval: 0) { false } }
+ .to output(/ended retry_on_exception$/).to_stdout_from_any_process
+ end
+ end
+
+ it 'does not repeat if no exception is raised' do
+ loop_counter = 0
+ return_value = "test passed"
+
+ expect(
+ subject.retry_on_exception(max_attempts: 2) do
+ loop_counter += 1
+ return_value
+ end
+ ).to eq(return_value)
+ expect(loop_counter).to eq(1)
+ end
+
+ it 'sets retry_on_exception to true' do
+ expect(subject).to receive(:repeat_until).with(hash_including(retry_on_exception: true))
+
+ subject.retry_on_exception
+ end
+
+ it 'sets max_attempts to 3 by default' do
+ expect(subject).to receive(:repeat_until).with(hash_including(max_attempts: 3))
+
+ subject.retry_on_exception
+ end
+
+ it 'sets sleep_interval to 0.5 by default' do
+ expect(subject).to receive(:repeat_until).with(hash_including(sleep_interval: 0.5))
+
+ subject.retry_on_exception
+ end
+ end
+end
diff --git a/qa/spec/shared_examples/scenario_shared_examples.rb b/qa/spec/support/shared_examples/scenario_shared_examples.rb
index 697e6cb39c8..17469ea470c 100644
--- a/qa/spec/shared_examples/scenario_shared_examples.rb
+++ b/qa/spec/support/shared_examples/scenario_shared_examples.rb
@@ -31,12 +31,6 @@ shared_examples 'a QA scenario class' do
expect(attributes).to have_received(:define).with(:gitlab_address, 'http://gitlab_address').at_least(:once)
end
- it 'performs before hooks' do
- subject.perform(args)
-
- expect(release).to have_received(:perform_before_hooks)
- end
-
it 'sets tags on runner' do
subject.perform(args)
diff --git a/qa/spec/support/waiter_spec.rb b/qa/spec/support/waiter_spec.rb
index 8283b65e1be..06e404c862a 100644
--- a/qa/spec/support/waiter_spec.rb
+++ b/qa/spec/support/waiter_spec.rb
@@ -9,29 +9,53 @@ describe QA::Support::Waiter do
QA::Runtime::Logger.logger = logger
end
- describe '.wait' do
+ describe '.wait_until' do
context 'when the condition is true' do
it 'logs the start' do
- expect { subject.wait(max: 0) {} }
- .to output(/with wait: max 0; interval 0.1/).to_stdout_from_any_process
+ expect { subject.wait_until(max_duration: 0, raise_on_failure: false) { true } }
+ .to output(/with wait_until: max_duration: 0; reload_page: ; sleep_interval: 0.1/).to_stdout_from_any_process
end
it 'logs the end' do
- expect { subject.wait(max: 0) {} }
- .to output(/ended wait after .* seconds$/).to_stdout_from_any_process
+ expect { subject.wait_until(max_duration: 0, raise_on_failure: false) { true } }
+ .to output(/ended wait_until$/).to_stdout_from_any_process
end
end
context 'when the condition is false' do
it 'logs the start' do
- expect { subject.wait(max: 0) { false } }
- .to output(/with wait: max 0; interval 0.1/).to_stdout_from_any_process
+ expect { subject.wait_until(max_duration: 0, raise_on_failure: false) { false } }
+ .to output(/with wait_until: max_duration: 0; reload_page: ; sleep_interval: 0.1/).to_stdout_from_any_process
end
it 'logs the end' do
- expect { subject.wait(max: 0) { false } }
- .to output(/ended wait after .* seconds$/).to_stdout_from_any_process
+ expect { subject.wait_until(max_duration: 0, raise_on_failure: false) { false } }
+ .to output(/ended wait_until$/).to_stdout_from_any_process
end
end
+
+ it 'sets max_duration to 60 by default' do
+ expect(subject).to receive(:repeat_until).with(hash_including(max_duration: 60))
+
+ subject.wait_until
+ end
+
+ it 'sets sleep_interval to 0.1 by default' do
+ expect(subject).to receive(:repeat_until).with(hash_including(sleep_interval: 0.1))
+
+ subject.wait_until
+ end
+
+ it 'sets raise_on_failure to false by default' do
+ expect(subject).to receive(:repeat_until).with(hash_including(raise_on_failure: false))
+
+ subject.wait_until
+ end
+
+ it 'sets retry_on_exception to false by default' do
+ expect(subject).to receive(:repeat_until).with(hash_including(retry_on_exception: false))
+
+ subject.wait_until
+ end
end
end