summaryrefslogtreecommitdiff
path: root/qa
diff options
context:
space:
mode:
Diffstat (limited to 'qa')
-rw-r--r--qa/Dockerfile12
-rw-r--r--qa/Gemfile.lock4
-rw-r--r--qa/README.md9
-rw-r--r--qa/qa.rb130
-rw-r--r--qa/qa/factory/base.rb60
-rw-r--r--qa/qa/factory/dependency.rb39
-rw-r--r--qa/qa/factory/product.rb30
-rw-r--r--qa/qa/factory/resource/branch.rb77
-rw-r--r--qa/qa/factory/resource/deploy_key.rb43
-rw-r--r--qa/qa/factory/resource/file.rb34
-rw-r--r--qa/qa/factory/resource/fork.rb29
-rw-r--r--qa/qa/factory/resource/group.rb41
-rw-r--r--qa/qa/factory/resource/issue.rb32
-rw-r--r--qa/qa/factory/resource/kubernetes_cluster.rb59
-rw-r--r--qa/qa/factory/resource/merge_request.rb65
-rw-r--r--qa/qa/factory/resource/merge_request_from_fork.rb24
-rw-r--r--qa/qa/factory/resource/personal_access_token.rb27
-rw-r--r--qa/qa/factory/resource/project.rb54
-rw-r--r--qa/qa/factory/resource/project_imported_from_github.rb37
-rw-r--r--qa/qa/factory/resource/project_milestone.rb36
-rw-r--r--qa/qa/factory/resource/runner.rb47
-rw-r--r--qa/qa/factory/resource/sandbox.rb34
-rw-r--r--qa/qa/factory/resource/secret_variable.rb28
-rw-r--r--qa/qa/factory/resource/ssh_key.rb40
-rw-r--r--qa/qa/factory/resource/user.rb58
-rw-r--r--qa/qa/factory/resource/wiki.rb25
-rw-r--r--qa/qa/factory/settings/hashed_storage.rb24
-rw-r--r--qa/qa/fixtures/auto_devops_rack/Gemfile.lock4
-rw-r--r--qa/qa/git/repository.rb156
-rw-r--r--qa/qa/page/README.md40
-rw-r--r--qa/qa/page/admin/menu.rb47
-rw-r--r--qa/qa/page/admin/settings/component/repository_storage.rb26
-rw-r--r--qa/qa/page/admin/settings/main.rb21
-rw-r--r--qa/qa/page/admin/settings/repository.rb23
-rw-r--r--qa/qa/page/admin/settings/repository_storage.rb23
-rw-r--r--qa/qa/page/base.rb32
-rw-r--r--qa/qa/page/component/clone_panel.rb4
-rw-r--r--qa/qa/page/component/dropdown_filter.rb18
-rw-r--r--qa/qa/page/component/groups_filter.rb24
-rw-r--r--qa/qa/page/component/issuable/common.rb35
-rw-r--r--qa/qa/page/component/select2.rb7
-rw-r--r--qa/qa/page/component/users_select.rb14
-rw-r--r--qa/qa/page/dashboard/groups.rb12
-rw-r--r--qa/qa/page/dashboard/projects.rb2
-rw-r--r--qa/qa/page/file/form.rb34
-rw-r--r--qa/qa/page/file/shared/commit_message.rb2
-rw-r--r--qa/qa/page/file/show.rb6
-rw-r--r--qa/qa/page/group/new.rb10
-rw-r--r--qa/qa/page/group/show.rb45
-rw-r--r--qa/qa/page/issuable/sidebar.rb4
-rw-r--r--qa/qa/page/label/index.rb15
-rw-r--r--qa/qa/page/label/new.rb30
-rw-r--r--qa/qa/page/layout/banner.rb2
-rw-r--r--qa/qa/page/main/login.rb49
-rw-r--r--qa/qa/page/main/menu.rb (renamed from qa/qa/page/menu/main.rb)23
-rw-r--r--qa/qa/page/main/oauth.rb2
-rw-r--r--qa/qa/page/main/sign_up.rb36
-rw-r--r--qa/qa/page/menu/admin.rb15
-rw-r--r--qa/qa/page/merge_request/new.rb10
-rw-r--r--qa/qa/page/merge_request/show.rb84
-rw-r--r--qa/qa/page/profile/menu.rb (renamed from qa/qa/page/menu/profile.rb)14
-rw-r--r--qa/qa/page/profile/personal_access_tokens.rb10
-rw-r--r--qa/qa/page/project/activity.rb2
-rw-r--r--qa/qa/page/project/fork/new.rb2
-rw-r--r--qa/qa/page/project/import/github.rb12
-rw-r--r--qa/qa/page/project/issue/index.rb2
-rw-r--r--qa/qa/page/project/issue/new.rb6
-rw-r--r--qa/qa/page/project/issue/show.rb38
-rw-r--r--qa/qa/page/project/job/show.rb25
-rw-r--r--qa/qa/page/project/menu.rb (renamed from qa/qa/page/menu/side.rb)73
-rw-r--r--qa/qa/page/project/new.rb16
-rw-r--r--qa/qa/page/project/operations/environments/index.rb23
-rw-r--r--qa/qa/page/project/operations/environments/show.rb23
-rw-r--r--qa/qa/page/project/operations/kubernetes/add.rb4
-rw-r--r--qa/qa/page/project/operations/kubernetes/add_existing.rb17
-rw-r--r--qa/qa/page/project/operations/kubernetes/index.rb4
-rw-r--r--qa/qa/page/project/operations/kubernetes/show.rb8
-rw-r--r--qa/qa/page/project/pipeline/index.rb2
-rw-r--r--qa/qa/page/project/pipeline/show.rb12
-rw-r--r--qa/qa/page/project/settings/advanced.rb6
-rw-r--r--qa/qa/page/project/settings/ci_cd.rb14
-rw-r--r--qa/qa/page/project/settings/ci_variables.rb (renamed from qa/qa/page/project/settings/secret_variables.rb)14
-rw-r--r--qa/qa/page/project/settings/common.rb2
-rw-r--r--qa/qa/page/project/settings/deploy_keys.rb12
-rw-r--r--qa/qa/page/project/settings/deploy_tokens.rb64
-rw-r--r--qa/qa/page/project/settings/members.rb27
-rw-r--r--qa/qa/page/project/settings/repository.rb6
-rw-r--r--qa/qa/page/project/settings/runners.rb6
-rw-r--r--qa/qa/page/project/show.rb38
-rw-r--r--qa/qa/page/project/web_ide/edit.rb79
-rw-r--r--qa/qa/page/project/wiki/edit.rb6
-rw-r--r--qa/qa/page/project/wiki/new.rb12
-rw-r--r--qa/qa/page/project/wiki/show.rb2
-rw-r--r--qa/qa/resource/README.md392
-rw-r--r--qa/qa/resource/api_fabricator.rb96
-rw-r--r--qa/qa/resource/base.rb155
-rw-r--r--qa/qa/resource/branch.rb77
-rw-r--r--qa/qa/resource/ci_variable.rb30
-rw-r--r--qa/qa/resource/deploy_key.rb43
-rw-r--r--qa/qa/resource/deploy_token.rb50
-rw-r--r--qa/qa/resource/file.rb36
-rw-r--r--qa/qa/resource/fork.rb43
-rw-r--r--qa/qa/resource/group.rb72
-rw-r--r--qa/qa/resource/issue.rb30
-rw-r--r--qa/qa/resource/kubernetes_cluster.rb57
-rw-r--r--qa/qa/resource/label.rb39
-rw-r--r--qa/qa/resource/merge_request.rb75
-rw-r--r--qa/qa/resource/merge_request_from_fork.rb31
-rw-r--r--qa/qa/resource/personal_access_token.rb27
-rw-r--r--qa/qa/resource/project.rb80
-rw-r--r--qa/qa/resource/project_imported_from_github.rb36
-rw-r--r--qa/qa/resource/project_milestone.rb36
-rw-r--r--qa/qa/resource/repository/project_push.rb (renamed from qa/qa/factory/repository/project_push.rb)22
-rw-r--r--qa/qa/resource/repository/push.rb (renamed from qa/qa/factory/repository/push.rb)20
-rw-r--r--qa/qa/resource/repository/wiki_push.rb (renamed from qa/qa/factory/repository/wiki_push.rb)16
-rw-r--r--qa/qa/resource/runner.rb49
-rw-r--r--qa/qa/resource/sandbox.rb60
-rw-r--r--qa/qa/resource/settings/hashed_storage.rb26
-rw-r--r--qa/qa/resource/ssh_key.rb26
-rw-r--r--qa/qa/resource/user.rb113
-rw-r--r--qa/qa/resource/wiki.rb30
-rw-r--r--qa/qa/runtime/api/client.rb29
-rw-r--r--qa/qa/runtime/browser.rb13
-rw-r--r--qa/qa/runtime/env.rb85
-rw-r--r--qa/qa/runtime/fixtures.rb19
-rw-r--r--qa/qa/runtime/logger.rb25
-rw-r--r--qa/qa/runtime/namespace.rb2
-rw-r--r--qa/qa/runtime/path.rb13
-rw-r--r--qa/qa/scenario/test/integration/instance_saml.rb13
-rw-r--r--qa/qa/scenario/test/integration/ldap_no_tls.rb13
-rw-r--r--qa/qa/scenario/test/integration/ldap_tls.rb (renamed from qa/qa/scenario/test/integration/ldap.rb)6
-rw-r--r--qa/qa/service/kubernetes_cluster.rb72
-rw-r--r--qa/qa/service/shellout.rb6
-rw-r--r--qa/qa/specs/features/api/1_manage/users_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/login/log_in_spec.rb4
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/login/log_into_gitlab_via_ldap_spec.rb4
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/login/log_into_mattermost_via_gitlab_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/login/login_via_instance_wide_saml_sso_spec.rb17
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb30
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb26
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb8
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb12
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/project/view_project_activity_spec.rb6
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb7
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb39
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb28
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb6
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb11
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb9
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb77
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb10
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb7
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/create_edit_delete_file_via_web_spec.rb4
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_http_spec.rb49
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_ssh_spec.rb84
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb32
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb6
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb10
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/use_ssh_key_spec.rb10
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb88
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb8
-rw-r--r--qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_ci_variable_spec.rb (renamed from qa/qa/specs/features/browser_ui/4_verify/secret_variable/add_secret_variable_spec.rb)16
-rw-r--r--qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb10
-rw-r--r--qa/qa/specs/features/browser_ui/4_verify/runner/register_runner_spec.rb4
-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.rb16
-rw-r--r--qa/qa/specs/features/browser_ui/6_release/deploy_token/add_deploy_token_spec.rb23
-rw-r--r--qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb108
-rw-r--r--qa/qa/specs/features/browser_ui/7_configure/mattermost/create_group_with_mattermost_team_spec.rb4
-rw-r--r--qa/qa/specs/features/sanity/framework_spec.rb4
-rw-r--r--qa/qa/specs/runner.rb6
-rw-r--r--qa/qa/support/api.rb28
-rw-r--r--qa/qa/support/page/logging.rb100
-rw-r--r--qa/qa/vendor/saml_idp/page/base.rb14
-rw-r--r--qa/qa/vendor/saml_idp/page/login.rb19
-rw-r--r--qa/spec/factory/base_spec.rb132
-rw-r--r--qa/spec/factory/dependency_spec.rb72
-rw-r--r--qa/spec/factory/product_spec.rb39
-rw-r--r--qa/spec/factory/resource/user_spec.rb36
-rw-r--r--qa/spec/git/repository_spec.rb43
-rw-r--r--qa/spec/page/base_spec.rb6
-rw-r--r--qa/spec/page/logging_spec.rb94
-rw-r--r--qa/spec/page/validator_spec.rb2
-rw-r--r--qa/spec/page/view_spec.rb4
-rw-r--r--qa/spec/resource/api_fabricator_spec.rb161
-rw-r--r--qa/spec/resource/base_spec.rb246
-rw-r--r--qa/spec/resource/repository/push_spec.rb26
-rw-r--r--qa/spec/runtime/api/client_spec.rb25
-rw-r--r--qa/spec/runtime/api/request_spec.rb18
-rw-r--r--qa/spec/runtime/api_request_spec.rb0
-rw-r--r--qa/spec/runtime/env_spec.rb132
-rw-r--r--qa/spec/runtime/logger_spec.rb33
-rw-r--r--qa/spec/scenario/test/integration/instance_saml_spec.rb9
-rw-r--r--qa/spec/scenario/test/integration/ldap_spec.rb12
-rw-r--r--qa/spec/spec_helper.rb4
-rw-r--r--qa/spec/specs/runner_spec.rb28
196 files changed, 4850 insertions, 1821 deletions
diff --git a/qa/Dockerfile b/qa/Dockerfile
index abf2184e1e2..9956ced0ef6 100644
--- a/qa/Dockerfile
+++ b/qa/Dockerfile
@@ -3,10 +3,20 @@ LABEL maintainer "Grzegorz Bizon <grzegorz@gitlab.com>"
ENV DEBIAN_FRONTEND noninteractive
##
+# Add support for stretch-backports
+#
+RUN echo "deb http://ftp.debian.org/debian stretch-backports main" >> /etc/apt/sources.list
+
+##
# Update APT sources and install some dependencies
#
RUN sed -i "s/httpredir.debian.org/ftp.us.debian.org/" /etc/apt/sources.list
-RUN apt-get update && apt-get install -y wget git unzip xvfb
+RUN apt-get update && apt-get install -y wget unzip xvfb
+
+##
+# Install some packages from backports
+#
+RUN apt-get -y -t stretch-backports install git git-lfs
##
# Install Docker
diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock
index 8d28fcacc05..d61ecf8fbb5 100644
--- a/qa/Gemfile.lock
+++ b/qa/Gemfile.lock
@@ -56,7 +56,7 @@ GEM
byebug (~> 9.1)
pry (~> 0.10)
public_suffix (3.0.1)
- rack (2.0.3)
+ rack (2.0.6)
rack-test (0.8.2)
rack (>= 1.0, < 3)
rake (12.3.0)
@@ -103,4 +103,4 @@ DEPENDENCIES
selenium-webdriver (~> 3.8.0)
BUNDLED WITH
- 1.16.4
+ 1.17.1
diff --git a/qa/README.md b/qa/README.md
index 746bd5cf94b..08ba59e117d 100644
--- a/qa/README.md
+++ b/qa/README.md
@@ -80,6 +80,15 @@ GITLAB_USERNAME=jsmith GITLAB_PASSWORD=password GITLAB_SANDBOX_NAME=jsmith-qa-sa
All [supported environment variables are here](https://gitlab.com/gitlab-org/gitlab-qa/blob/master/docs/what_tests_can_be_run.md#supported-environment-variables).
+### Sending additional cookies
+
+The environment variable `QA_COOKIES` can be set to send additional cookies
+on every request. This is necessary on gitlab.com to direct traffic to the
+canary fleet. To do this set `QA_COOKIES="gitlab_canary=true"`.
+
+To set multiple cookies, separate them with the `;` character, for example: `QA_COOKIES="cookie1=value;cookie2=value2"`
+
+
### Building a Docker image to test
Once you have made changes to the CE/EE repositories, you may want to build a
diff --git a/qa/qa.rb b/qa/qa.rb
index 952084085d5..aa0b78b37e8 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -16,6 +16,9 @@ module QA
autoload :Browser, 'qa/runtime/browser'
autoload :Env, 'qa/runtime/env'
autoload :Address, 'qa/runtime/address'
+ autoload :Path, 'qa/runtime/path'
+ autoload :Fixtures, 'qa/runtime/fixtures'
+ autoload :Logger, 'qa/runtime/logger'
module API
autoload :Client, 'qa/runtime/api/client'
@@ -33,41 +36,40 @@ module QA
##
# GitLab QA fabrication mechanisms
#
- module Factory
- autoload :Base, 'qa/factory/base'
- autoload :Dependency, 'qa/factory/dependency'
- autoload :Product, 'qa/factory/product'
-
- module Resource
- autoload :Sandbox, 'qa/factory/resource/sandbox'
- autoload :Group, 'qa/factory/resource/group'
- autoload :Issue, 'qa/factory/resource/issue'
- autoload :Project, 'qa/factory/resource/project'
- autoload :MergeRequest, 'qa/factory/resource/merge_request'
- autoload :ProjectImportedFromGithub, 'qa/factory/resource/project_imported_from_github'
- autoload :MergeRequestFromFork, 'qa/factory/resource/merge_request_from_fork'
- autoload :DeployKey, 'qa/factory/resource/deploy_key'
- autoload :Branch, 'qa/factory/resource/branch'
- autoload :SecretVariable, 'qa/factory/resource/secret_variable'
- autoload :Runner, 'qa/factory/resource/runner'
- autoload :PersonalAccessToken, 'qa/factory/resource/personal_access_token'
- autoload :KubernetesCluster, 'qa/factory/resource/kubernetes_cluster'
- autoload :User, 'qa/factory/resource/user'
- autoload :ProjectMilestone, 'qa/factory/resource/project_milestone'
- autoload :Wiki, 'qa/factory/resource/wiki'
- autoload :File, 'qa/factory/resource/file'
- autoload :Fork, 'qa/factory/resource/fork'
- autoload :SSHKey, 'qa/factory/resource/ssh_key'
- end
+ module Resource
+ autoload :ApiFabricator, 'qa/resource/api_fabricator'
+ autoload :Base, 'qa/resource/base'
+
+ autoload :Sandbox, 'qa/resource/sandbox'
+ autoload :Group, 'qa/resource/group'
+ autoload :Issue, 'qa/resource/issue'
+ autoload :Project, 'qa/resource/project'
+ autoload :Label, 'qa/resource/label'
+ autoload :MergeRequest, 'qa/resource/merge_request'
+ autoload :ProjectImportedFromGithub, 'qa/resource/project_imported_from_github'
+ autoload :MergeRequestFromFork, 'qa/resource/merge_request_from_fork'
+ autoload :DeployKey, 'qa/resource/deploy_key'
+ autoload :DeployToken, 'qa/resource/deploy_token'
+ autoload :Branch, 'qa/resource/branch'
+ autoload :CiVariable, 'qa/resource/ci_variable'
+ autoload :Runner, 'qa/resource/runner'
+ autoload :PersonalAccessToken, 'qa/resource/personal_access_token'
+ autoload :KubernetesCluster, 'qa/resource/kubernetes_cluster'
+ autoload :User, 'qa/resource/user'
+ autoload :ProjectMilestone, 'qa/resource/project_milestone'
+ autoload :Wiki, 'qa/resource/wiki'
+ autoload :File, 'qa/resource/file'
+ autoload :Fork, 'qa/resource/fork'
+ autoload :SSHKey, 'qa/resource/ssh_key'
module Repository
- autoload :Push, 'qa/factory/repository/push'
- autoload :ProjectPush, 'qa/factory/repository/project_push'
- autoload :WikiPush, 'qa/factory/repository/wiki_push'
+ autoload :Push, 'qa/resource/repository/push'
+ autoload :ProjectPush, 'qa/resource/repository/project_push'
+ autoload :WikiPush, 'qa/resource/repository/wiki_push'
end
module Settings
- autoload :HashedStorage, 'qa/factory/settings/hashed_storage'
+ autoload :HashedStorage, 'qa/resource/settings/hashed_storage'
end
end
@@ -94,7 +96,9 @@ module QA
module Integration
autoload :Github, 'qa/scenario/test/integration/github'
- autoload :LDAP, 'qa/scenario/test/integration/ldap'
+ autoload :LDAPNoTLS, 'qa/scenario/test/integration/ldap_no_tls'
+ autoload :LDAPTLS, 'qa/scenario/test/integration/ldap_tls'
+ autoload :InstanceSAML, 'qa/scenario/test/integration/instance_saml'
autoload :Kubernetes, 'qa/scenario/test/integration/kubernetes'
autoload :Mattermost, 'qa/scenario/test/integration/mattermost'
autoload :ObjectStorage, 'qa/scenario/test/integration/object_storage'
@@ -120,6 +124,7 @@ module QA
module Main
autoload :Login, 'qa/page/main/login'
+ autoload :Menu, 'qa/page/main/menu'
autoload :OAuth, 'qa/page/main/oauth'
autoload :SignUp, 'qa/page/main/sign_up'
end
@@ -128,13 +133,6 @@ module QA
autoload :Common, 'qa/page/settings/common'
end
- module Menu
- autoload :Main, 'qa/page/menu/main'
- autoload :Side, 'qa/page/menu/side'
- autoload :Admin, 'qa/page/menu/admin'
- autoload :Profile, 'qa/page/menu/profile'
- end
-
module Dashboard
autoload :Projects, 'qa/page/dashboard/projects'
autoload :Groups, 'qa/page/dashboard/groups'
@@ -158,6 +156,7 @@ module QA
autoload :New, 'qa/page/project/new'
autoload :Show, 'qa/page/project/show'
autoload :Activity, 'qa/page/project/activity'
+ autoload :Menu, 'qa/page/project/menu'
module Import
autoload :Github, 'qa/page/project/import/github'
@@ -179,10 +178,12 @@ module QA
autoload :Repository, 'qa/page/project/settings/repository'
autoload :CICD, 'qa/page/project/settings/ci_cd'
autoload :DeployKeys, 'qa/page/project/settings/deploy_keys'
+ autoload :DeployTokens, 'qa/page/project/settings/deploy_tokens'
autoload :ProtectedBranches, 'qa/page/project/settings/protected_branches'
- autoload :SecretVariables, 'qa/page/project/settings/secret_variables'
+ autoload :CiVariables, 'qa/page/project/settings/ci_variables'
autoload :Runners, 'qa/page/project/settings/runners'
autoload :MergeRequest, 'qa/page/project/settings/merge_request'
+ autoload :Members, 'qa/page/project/settings/members'
end
module Issue
@@ -201,6 +202,11 @@ module QA
end
module Operations
+ module Environments
+ autoload :Index, 'qa/page/project/operations/environments/index'
+ autoload :Show, 'qa/page/project/operations/environments/show'
+ end
+
module Kubernetes
autoload :Index, 'qa/page/project/operations/kubernetes/index'
autoload :Add, 'qa/page/project/operations/kubernetes/add'
@@ -214,9 +220,14 @@ module QA
autoload :New, 'qa/page/project/wiki/new'
autoload :Show, 'qa/page/project/wiki/show'
end
+
+ module WebIDE
+ autoload :Edit, 'qa/page/project/web_ide/edit'
+ end
end
module Profile
+ autoload :Menu, 'qa/page/profile/menu'
autoload :PersonalAccessTokens, 'qa/page/profile/personal_access_tokens'
autoload :SSHKeys, 'qa/page/profile/ssh_keys'
end
@@ -229,15 +240,25 @@ module QA
autoload :Banner, 'qa/page/layout/banner'
end
+ module Label
+ autoload :New, 'qa/page/label/new'
+ autoload :Index, 'qa/page/label/index'
+ end
+
module MergeRequest
autoload :New, 'qa/page/merge_request/new'
autoload :Show, 'qa/page/merge_request/show'
end
module Admin
+ autoload :Menu, 'qa/page/admin/menu'
+
module Settings
- autoload :RepositoryStorage, 'qa/page/admin/settings/repository_storage'
- autoload :Main, 'qa/page/admin/settings/main'
+ autoload :Repository, 'qa/page/admin/settings/repository'
+
+ module Component
+ autoload :RepositoryStorage, 'qa/page/admin/settings/component/repository_storage'
+ end
end
end
@@ -254,6 +275,12 @@ module QA
autoload :Dropzone, 'qa/page/component/dropzone'
autoload :GroupsFilter, 'qa/page/component/groups_filter'
autoload :Select2, 'qa/page/component/select2'
+ autoload :DropdownFilter, 'qa/page/component/dropdown_filter'
+ autoload :UsersSelect, 'qa/page/component/users_select'
+
+ module Issuable
+ autoload :Common, 'qa/page/component/issuable/common'
+ end
end
end
@@ -283,6 +310,27 @@ module QA
autoload :Config, 'qa/specs/config'
autoload :Runner, 'qa/specs/runner'
end
+
+ ##
+ # Classes that describe the structure of vendor/third party application pages
+ #
+ module Vendor
+ module SAMLIdp
+ module Page
+ autoload :Base, 'qa/vendor/saml_idp/page/base'
+ autoload :Login, 'qa/vendor/saml_idp/page/login'
+ end
+ end
+ end
+
+ # Classes that provide support to other parts of the framework.
+ #
+ module Support
+ module Page
+ autoload :Logging, 'qa/support/page/logging'
+ end
+ autoload :Api, 'qa/support/api'
+ end
end
QA::Runtime::Release.extend_autoloads!
diff --git a/qa/qa/factory/base.rb b/qa/qa/factory/base.rb
deleted file mode 100644
index 7a532ce534b..00000000000
--- a/qa/qa/factory/base.rb
+++ /dev/null
@@ -1,60 +0,0 @@
-require 'forwardable'
-
-module QA
- module Factory
- class Base
- extend SingleForwardable
-
- def_delegators :evaluator, :dependency, :dependencies
- def_delegators :evaluator, :product, :attributes
-
- def fabricate!(*_args)
- raise NotImplementedError
- end
-
- def self.fabricate!(*args)
- new.tap do |factory|
- yield factory if block_given?
-
- dependencies.each do |name, signature|
- Factory::Dependency.new(name, factory, signature).build!
- end
-
- factory.fabricate!(*args)
-
- break Factory::Product.populate!(factory)
- end
- end
-
- def self.evaluator
- @evaluator ||= Factory::Base::DSL.new(self)
- end
-
- class DSL
- attr_reader :dependencies, :attributes
-
- def initialize(base)
- @base = base
- @dependencies = {}
- @attributes = {}
- end
-
- def dependency(factory, as:, &block)
- as.tap do |name|
- @base.class_eval { attr_accessor name }
-
- Dependency::Signature.new(factory, block).tap do |signature|
- @dependencies.store(name, signature)
- end
- end
- end
-
- def product(attribute, &block)
- Product::Attribute.new(attribute, block).tap do |signature|
- @attributes.store(attribute, signature)
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/dependency.rb b/qa/qa/factory/dependency.rb
deleted file mode 100644
index fc5dc82ce29..00000000000
--- a/qa/qa/factory/dependency.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-module QA
- module Factory
- class Dependency
- Signature = Struct.new(:factory, :block)
-
- def initialize(name, factory, signature)
- @name = name
- @factory = factory
- @signature = signature
- end
-
- def overridden?
- !!@factory.public_send(@name)
- end
-
- def build!
- return if overridden?
-
- Builder.new(@signature, @factory).fabricate!.tap do |product|
- @factory.public_send("#{@name}=", product)
- end
- end
-
- class Builder
- def initialize(signature, caller_factory)
- @factory = signature.factory
- @block = signature.block
- @caller_factory = caller_factory
- end
-
- def fabricate!
- @factory.fabricate! do |factory|
- @block&.call(factory, @caller_factory)
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/product.rb b/qa/qa/factory/product.rb
deleted file mode 100644
index 996b7f14f61..00000000000
--- a/qa/qa/factory/product.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-require 'capybara/dsl'
-
-module QA
- module Factory
- class Product
- include Capybara::DSL
-
- Attribute = Struct.new(:name, :block)
-
- def initialize
- @location = current_url
- end
-
- def visit!
- visit @location
- end
-
- def self.populate!(factory)
- new.tap do |product|
- factory.class.attributes.each_value do |attribute|
- product.instance_exec(factory, attribute.block) do |factory, block|
- value = block.call(factory)
- product.define_singleton_method(attribute.name) { value }
- end
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/resource/branch.rb b/qa/qa/factory/resource/branch.rb
deleted file mode 100644
index 60539992073..00000000000
--- a/qa/qa/factory/resource/branch.rb
+++ /dev/null
@@ -1,77 +0,0 @@
-module QA
- module Factory
- module Resource
- class Branch < Factory::Base
- attr_accessor :project, :branch_name,
- :allow_to_push, :allow_to_merge, :protected
-
- dependency Factory::Resource::Project, as: :project do |project|
- project.name = 'protected-branch-project'
- end
-
- def initialize
- @branch_name = 'test/branch'
- @allow_to_push = true
- @allow_to_merge = true
- @protected = false
- end
-
- def fabricate!
- project.visit!
-
- Factory::Repository::ProjectPush.fabricate! do |resource|
- resource.project = project
- resource.file_name = 'kick-off.txt'
- resource.commit_message = 'First commit'
- end
-
- branch = Factory::Repository::ProjectPush.fabricate! do |resource|
- resource.project = project
- resource.file_name = 'README.md'
- resource.commit_message = 'Add readme'
- resource.branch_name = 'master'
- resource.new_branch = false
- resource.remote_branch = @branch_name
- end
-
- Page::Project::Show.perform do |page|
- page.wait { page.has_content?(branch_name) }
- end
-
- # The upcoming process will make it access the Protected Branches page,
- # select the already created branch and protect it according
- # to `allow_to_push` variable.
- return branch unless @protected
-
- Page::Menu::Side.act do
- click_repository_settings
- end
-
- Page::Project::Settings::Repository.perform do |setting|
- setting.expand_protected_branches do |page|
- page.select_branch(branch_name)
-
- if allow_to_push
- page.allow_devs_and_maintainers_to_push
- else
- page.allow_no_one_to_push
- end
-
- if allow_to_merge
- page.allow_devs_and_maintainers_to_merge
- else
- page.allow_no_one_to_merge
- end
-
- page.wait(reload: false) do
- !page.first('.btn-success').disabled?
- end
-
- page.protect_branch
- end
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/resource/deploy_key.rb b/qa/qa/factory/resource/deploy_key.rb
deleted file mode 100644
index ea8a3ad687d..00000000000
--- a/qa/qa/factory/resource/deploy_key.rb
+++ /dev/null
@@ -1,43 +0,0 @@
-module QA
- module Factory
- module Resource
- class DeployKey < Factory::Base
- attr_accessor :title, :key
-
- product :fingerprint do |resource|
- Page::Project::Settings::Repository.act do
- expand_deploy_keys do |key|
- key_offset = key.key_titles.index do |title|
- title.text == resource.title
- end
-
- key.key_fingerprints[key_offset].text
- end
- end
- end
-
- dependency Factory::Resource::Project, as: :project do |project|
- project.name = 'project-to-deploy'
- project.description = 'project for adding deploy key test'
- end
-
- def fabricate!
- project.visit!
-
- Page::Menu::Side.act do
- click_repository_settings
- end
-
- Page::Project::Settings::Repository.perform do |setting|
- setting.expand_deploy_keys do |page|
- page.fill_key_title(title)
- page.fill_key_value(key)
-
- page.add_key
- end
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/resource/file.rb b/qa/qa/factory/resource/file.rb
deleted file mode 100644
index 2016d10ddae..00000000000
--- a/qa/qa/factory/resource/file.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-module QA
- module Factory
- module Resource
- class File < Factory::Base
- attr_accessor :name,
- :content,
- :commit_message
-
- dependency Factory::Resource::Project, as: :project do |project|
- project.name = 'project-with-new-file'
- end
-
- def initialize
- @name = 'QA Test - File name'
- @content = 'QA Test - File content'
- @commit_message = 'QA Test - Commit message'
- end
-
- def fabricate!
- project.visit!
-
- Page::Project::Show.act { go_to_new_file! }
-
- Page::File::Form.perform do |page|
- page.add_name(@name)
- page.add_content(@content)
- page.add_commit_message(@commit_message)
- page.commit_changes
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/resource/fork.rb b/qa/qa/factory/resource/fork.rb
deleted file mode 100644
index 01969c31438..00000000000
--- a/qa/qa/factory/resource/fork.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-module QA
- module Factory
- module Resource
- class Fork < Factory::Base
- dependency Factory::Repository::ProjectPush, as: :push
-
- dependency Factory::Resource::User, as: :user do |user|
- if Runtime::Env.forker?
- user.username = Runtime::Env.forker_username
- user.password = Runtime::Env.forker_password
- end
- end
-
- product(:user) { |factory| factory.user }
-
- def fabricate!
- push.project.visit!
- Page::Project::Show.act { fork_project }
-
- Page::Project::Fork::New.perform do |fork_new|
- fork_new.choose_namespace(user.name)
- end
-
- Page::Layout::Banner.act { has_notice?('The project was successfully forked.') }
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/resource/group.rb b/qa/qa/factory/resource/group.rb
deleted file mode 100644
index 033fc48c08f..00000000000
--- a/qa/qa/factory/resource/group.rb
+++ /dev/null
@@ -1,41 +0,0 @@
-module QA
- module Factory
- module Resource
- class Group < Factory::Base
- attr_accessor :path, :description
-
- dependency Factory::Resource::Sandbox, as: :sandbox
-
- def initialize
- @path = Runtime::Namespace.name
- @description = "QA test run at #{Runtime::Namespace.time}"
- end
-
- def fabricate!
- sandbox.visit!
-
- Page::Group::Show.perform do |group_show|
- if group_show.has_subgroup?(path)
- group_show.go_to_subgroup(path)
- else
- group_show.go_to_new_subgroup
-
- Page::Group::New.perform do |group_new|
- group_new.set_path(path)
- group_new.set_description(description)
- group_new.set_visibility('Public')
- group_new.create
- end
-
- # Ensure that the group was actually created
- group_show.wait(time: 1) do
- group_show.has_text?(path) &&
- group_show.has_new_project_or_subgroup_dropdown?
- end
- end
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/resource/issue.rb b/qa/qa/factory/resource/issue.rb
deleted file mode 100644
index 95f48e20b3e..00000000000
--- a/qa/qa/factory/resource/issue.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-module QA
- module Factory
- module Resource
- class Issue < Factory::Base
- attr_writer :title, :description, :project
-
- dependency Factory::Resource::Project, as: :project do |project|
- project.name = 'project-for-issues'
- project.description = 'project for adding issues'
- end
-
- product :title do
- Page::Project::Issue::Show.act { issue_title }
- end
-
- def fabricate!
- project.visit!
-
- Page::Project::Show.act do
- go_to_new_issue
- end
-
- Page::Project::Issue::New.perform do |page|
- page.add_title(@title)
- page.add_description(@description)
- page.create_new_issue
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/resource/kubernetes_cluster.rb b/qa/qa/factory/resource/kubernetes_cluster.rb
deleted file mode 100644
index 94d7df7128b..00000000000
--- a/qa/qa/factory/resource/kubernetes_cluster.rb
+++ /dev/null
@@ -1,59 +0,0 @@
-require 'securerandom'
-
-module QA
- module Factory
- module Resource
- class KubernetesCluster < Factory::Base
- attr_writer :project, :cluster,
- :install_helm_tiller, :install_ingress, :install_prometheus, :install_runner
-
- product :ingress_ip do
- Page::Project::Operations::Kubernetes::Show.perform do |page|
- page.ingress_ip
- end
- end
-
- def fabricate!
- @project.visit!
-
- Page::Menu::Side.act { click_operations_kubernetes }
-
- Page::Project::Operations::Kubernetes::Index.perform do |page|
- page.add_kubernetes_cluster
- end
-
- Page::Project::Operations::Kubernetes::Add.perform do |page|
- page.add_existing_cluster
- end
-
- Page::Project::Operations::Kubernetes::AddExisting.perform do |page|
- page.set_cluster_name(@cluster.cluster_name)
- page.set_api_url(@cluster.api_url)
- page.set_ca_certificate(@cluster.ca_certificate)
- page.set_token(@cluster.token)
- page.add_cluster!
- end
-
- if @install_helm_tiller
- Page::Project::Operations::Kubernetes::Show.perform do |page|
- # We must wait a few seconds for permissions to be set up correctly for new cluster
- sleep 10
-
- # Helm must be installed before everything else
- page.install!(:helm)
- page.await_installed(:helm)
-
- page.install!(:ingress) if @install_ingress
- page.install!(:prometheus) if @install_prometheus
- page.install!(:runner) if @install_runner
-
- page.await_installed(:ingress) if @install_ingress
- page.await_installed(:prometheus) if @install_prometheus
- page.await_installed(:runner) if @install_runner
- end
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/resource/merge_request.rb b/qa/qa/factory/resource/merge_request.rb
deleted file mode 100644
index ddb62bd0a68..00000000000
--- a/qa/qa/factory/resource/merge_request.rb
+++ /dev/null
@@ -1,65 +0,0 @@
-require 'securerandom'
-
-module QA
- module Factory
- module Resource
- class MergeRequest < Factory::Base
- attr_accessor :title,
- :description,
- :source_branch,
- :target_branch,
- :assignee,
- :milestone,
- :labels
-
- product :project do |factory|
- factory.project
- end
-
- product :source_branch do |factory|
- factory.source_branch
- end
-
- dependency Factory::Resource::Project, as: :project do |project|
- project.name = 'project-with-merge-request'
- end
-
- dependency Factory::Repository::ProjectPush, as: :target do |push, factory|
- factory.project.visit!
- push.project = factory.project
- push.branch_name = 'master'
- push.remote_branch = factory.target_branch
- end
-
- dependency Factory::Repository::ProjectPush, as: :source do |push, factory|
- push.project = factory.project
- push.branch_name = factory.target_branch
- push.remote_branch = factory.source_branch
- push.file_name = "added_file.txt"
- push.file_content = "File Added"
- end
-
- def initialize
- @title = 'QA test - merge request'
- @description = 'This is a test merge request'
- @source_branch = "qa-test-feature-#{SecureRandom.hex(8)}"
- @target_branch = "master"
- @assignee = nil
- @milestone = nil
- @labels = []
- end
-
- def fabricate!
- project.visit!
- Page::Project::Show.act { new_merge_request }
- Page::MergeRequest::New.perform do |page|
- page.fill_title(@title)
- page.fill_description(@description)
- page.choose_milestone(@milestone) if @milestone
- page.create_merge_request
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/resource/merge_request_from_fork.rb b/qa/qa/factory/resource/merge_request_from_fork.rb
deleted file mode 100644
index 6caaf65f673..00000000000
--- a/qa/qa/factory/resource/merge_request_from_fork.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-module QA
- module Factory
- module Resource
- class MergeRequestFromFork < MergeRequest
- attr_accessor :fork_branch
-
- dependency Factory::Resource::Fork, as: :fork
-
- dependency Factory::Repository::ProjectPush, as: :push do |push, factory|
- push.project = factory.fork
- push.branch_name = factory.fork_branch
- push.file_name = 'file2.txt'
- push.user = factory.fork.user
- end
-
- def fabricate!
- fork.visit!
- Page::Project::Show.act { new_merge_request }
- Page::MergeRequest::New.act { create_merge_request }
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/resource/personal_access_token.rb b/qa/qa/factory/resource/personal_access_token.rb
deleted file mode 100644
index 514e3615d18..00000000000
--- a/qa/qa/factory/resource/personal_access_token.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-module QA
- module Factory
- module Resource
- ##
- # Create a personal access token that can be used by the api
- #
- class PersonalAccessToken < Factory::Base
- attr_accessor :name
-
- product :access_token do
- Page::Profile::PersonalAccessTokens.act { created_access_token }
- end
-
- def fabricate!
- Page::Menu::Main.act { go_to_profile_settings }
- Page::Menu::Profile.act { click_access_tokens }
-
- Page::Profile::PersonalAccessTokens.perform do |page|
- page.fill_token_name(name || 'api-test-token')
- page.check_api
- page.create_token
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/resource/project.rb b/qa/qa/factory/resource/project.rb
deleted file mode 100644
index 90db26ab3ab..00000000000
--- a/qa/qa/factory/resource/project.rb
+++ /dev/null
@@ -1,54 +0,0 @@
-require 'securerandom'
-
-module QA
- module Factory
- module Resource
- class Project < Factory::Base
- attr_writer :description
- attr_reader :name
-
- dependency Factory::Resource::Group, as: :group
-
- product :name do |factory|
- factory.name
- end
-
- product :repository_ssh_location do
- Page::Project::Show.act do
- choose_repository_clone_ssh
- repository_location
- end
- end
-
- product :repository_http_location do
- Page::Project::Show.act do
- choose_repository_clone_http
- repository_location
- end
- end
-
- def initialize
- @description = 'My awesome project'
- end
-
- def name=(raw_name)
- @name = "#{raw_name}-#{SecureRandom.hex(8)}"
- end
-
- def fabricate!
- group.visit!
-
- Page::Group::Show.act { go_to_new_project }
-
- Page::Project::New.perform do |page|
- page.choose_test_namespace
- page.choose_name(@name)
- page.add_description(@description)
- page.set_visibility('Public')
- page.create_new_project
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/resource/project_imported_from_github.rb b/qa/qa/factory/resource/project_imported_from_github.rb
deleted file mode 100644
index df2a3340d60..00000000000
--- a/qa/qa/factory/resource/project_imported_from_github.rb
+++ /dev/null
@@ -1,37 +0,0 @@
-require 'securerandom'
-
-module QA
- module Factory
- module Resource
- class ProjectImportedFromGithub < Resource::Project
- attr_writer :personal_access_token, :github_repository_path
-
- dependency Factory::Resource::Group, as: :group
-
- product :name do |factory|
- factory.name
- end
-
- def fabricate!
- group.visit!
-
- Page::Group::Show.act { go_to_new_project }
-
- Page::Project::New.perform do |page|
- page.go_to_import_project
- end
-
- Page::Project::New.perform do |page|
- page.go_to_github_import
- end
-
- Page::Project::Import::Github.perform do |page|
- page.add_personal_access_token(@personal_access_token)
- page.list_repos
- page.import!(@github_repository_path, @name)
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/resource/project_milestone.rb b/qa/qa/factory/resource/project_milestone.rb
deleted file mode 100644
index 47a5e74204f..00000000000
--- a/qa/qa/factory/resource/project_milestone.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-module QA
- module Factory
- module Resource
- class ProjectMilestone < Factory::Base
- attr_accessor :description
- attr_reader :title
-
- dependency Factory::Resource::Project, as: :project
-
- product(:title) { |factory| factory.title }
-
- def title=(title)
- @title = "#{title}-#{SecureRandom.hex(4)}"
- @description = 'A milestone'
- end
-
- def fabricate!
- project.visit!
-
- Page::Menu::Side.act do
- click_issues
- click_milestones
- end
-
- Page::Project::Milestone::Index.act { click_new_milestone }
-
- Page::Project::Milestone::New.perform do |milestone_new|
- milestone_new.set_title(@title)
- milestone_new.set_description(@description)
- milestone_new.create_new_milestone
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/resource/runner.rb b/qa/qa/factory/resource/runner.rb
deleted file mode 100644
index 03b69eb1bdf..00000000000
--- a/qa/qa/factory/resource/runner.rb
+++ /dev/null
@@ -1,47 +0,0 @@
-require 'securerandom'
-
-module QA
- module Factory
- module Resource
- class Runner < Factory::Base
- attr_writer :name, :tags, :image
-
- dependency Factory::Resource::Project, as: :project do |project|
- project.name = 'project-with-ci-cd'
- project.description = 'Project with CI/CD Pipelines'
- end
-
- def name
- @name || "qa-runner-#{SecureRandom.hex(4)}"
- end
-
- def tags
- @tags || %w[qa e2e]
- end
-
- def image
- @image || 'gitlab/gitlab-runner:alpine'
- end
-
- def fabricate!
- project.visit!
-
- Page::Menu::Side.act { click_ci_cd_settings }
-
- Service::Runner.new(name).tap do |runner|
- Page::Project::Settings::CICD.perform do |settings|
- settings.expand_runners_settings do |runners|
- runner.pull
- runner.token = runners.registration_token
- runner.address = runners.coordinator_address
- runner.tags = tags
- runner.image = image
- runner.register!
- end
- end
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/resource/sandbox.rb b/qa/qa/factory/resource/sandbox.rb
deleted file mode 100644
index 4f6039f300f..00000000000
--- a/qa/qa/factory/resource/sandbox.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-module QA
- module Factory
- module Resource
- ##
- # Ensure we're in our sandbox namespace, either by navigating to it or by
- # creating it if it doesn't yet exist.
- #
- class Sandbox < Factory::Base
- def initialize
- @name = Runtime::Namespace.sandbox_name
- end
-
- def fabricate!
- Page::Menu::Main.act { go_to_groups }
-
- Page::Dashboard::Groups.perform do |page|
- if page.has_group?(@name)
- page.go_to_group(@name)
- else
- page.go_to_new_group
-
- Page::Group::New.perform do |group|
- group.set_path(@name)
- group.set_description('GitLab QA Sandbox Group')
- group.set_visibility('Public')
- group.create
- end
- end
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/resource/secret_variable.rb b/qa/qa/factory/resource/secret_variable.rb
deleted file mode 100644
index 12a830da116..00000000000
--- a/qa/qa/factory/resource/secret_variable.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-module QA
- module Factory
- module Resource
- class SecretVariable < Factory::Base
- attr_accessor :key, :value
-
- dependency Factory::Resource::Project, as: :project do |project|
- project.name = 'project-with-secret-variables'
- project.description = 'project for adding secret variable test'
- end
-
- def fabricate!
- project.visit!
-
- Page::Menu::Side.act { click_ci_cd_settings }
-
- Page::Project::Settings::CICD.perform do |setting|
- setting.expand_secret_variables do |page|
- page.fill_variable(key, value)
-
- page.save_variables
- end
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/resource/ssh_key.rb b/qa/qa/factory/resource/ssh_key.rb
deleted file mode 100644
index 6c872f32d16..00000000000
--- a/qa/qa/factory/resource/ssh_key.rb
+++ /dev/null
@@ -1,40 +0,0 @@
-# frozen_string_literal: true
-
-module QA
- module Factory
- module Resource
- class SSHKey < Factory::Base
- extend Forwardable
-
- attr_accessor :title
- attr_reader :private_key, :public_key, :fingerprint
- def_delegators :key, :private_key, :public_key, :fingerprint
-
- product :private_key do |factory|
- factory.private_key
- end
-
- product :title do |factory|
- factory.title
- end
-
- product :fingerprint do |factory|
- factory.fingerprint
- end
-
- def key
- @key ||= Runtime::Key::RSA.new
- end
-
- def fabricate!
- Page::Menu::Main.act { go_to_profile_settings }
- Page::Menu::Profile.act { click_ssh_keys }
-
- Page::Profile::SSHKeys.perform do |page|
- page.add_key(public_key, title)
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/resource/user.rb b/qa/qa/factory/resource/user.rb
deleted file mode 100644
index eac2a873bd5..00000000000
--- a/qa/qa/factory/resource/user.rb
+++ /dev/null
@@ -1,58 +0,0 @@
-require 'securerandom'
-
-module QA
- module Factory
- module Resource
- class User < Factory::Base
- attr_reader :unique_id
- attr_writer :username, :password, :name, :email
-
- def initialize
- @unique_id = SecureRandom.hex(8)
- end
-
- def username
- @username ||= "qa-user-#{unique_id}"
- end
-
- def password
- @password ||= 'password'
- end
-
- def name
- @name ||= username
- end
-
- def email
- @email ||= "#{username}@example.com"
- end
-
- def credentials_given?
- defined?(@username) && defined?(@password)
- end
-
- product(:name) { |factory| factory.name }
- product(:username) { |factory| factory.username }
- product(:email) { |factory| factory.email }
- product(:password) { |factory| factory.password }
-
- def fabricate!
- Page::Menu::Main.perform { |main| main.sign_out }
-
- if credentials_given?
- Page::Main::Login.perform do |login|
- login.sign_in_using_credentials(self)
- end
- else
- Page::Main::Login.perform do |login|
- login.switch_to_register_tab
- end
- Page::Main::SignUp.perform do |signup|
- signup.sign_up!(self)
- end
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/resource/wiki.rb b/qa/qa/factory/resource/wiki.rb
deleted file mode 100644
index cc200a512d5..00000000000
--- a/qa/qa/factory/resource/wiki.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-module QA
- module Factory
- module Resource
- class Wiki < Factory::Base
- attr_accessor :title, :content, :message
-
- dependency Factory::Resource::Project, as: :project do |project|
- project.name = 'project-for-wikis'
- project.description = 'project for adding wikis'
- end
-
- def fabricate!
- Page::Menu::Side.act { click_wiki }
- Page::Project::Wiki::New.perform do |page|
- page.go_to_create_first_page
- page.set_title(@title)
- page.set_content(@content)
- page.set_message(@message)
- page.create_new_page
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/settings/hashed_storage.rb b/qa/qa/factory/settings/hashed_storage.rb
deleted file mode 100644
index c69ebed3c6b..00000000000
--- a/qa/qa/factory/settings/hashed_storage.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-module QA
- module Factory
- module Settings
- class HashedStorage < Factory::Base
- def fabricate!(*traits)
- raise ArgumentError unless traits.include?(:enabled)
-
- Page::Main::Login.act { sign_in_using_credentials }
- Page::Menu::Main.act { go_to_admin_area }
- Page::Menu::Admin.act { go_to_settings }
-
- Page::Admin::Settings::Main.perform do |setting|
- setting.expand_repository_storage do |page|
- page.enable_hashed_storage
- page.save_settings
- end
- end
-
- QA::Page::Menu::Main.act { sign_out }
- end
- end
- end
- end
-end
diff --git a/qa/qa/fixtures/auto_devops_rack/Gemfile.lock b/qa/qa/fixtures/auto_devops_rack/Gemfile.lock
index 09cf72c48ac..d44ccbb5e69 100644
--- a/qa/qa/fixtures/auto_devops_rack/Gemfile.lock
+++ b/qa/qa/fixtures/auto_devops_rack/Gemfile.lock
@@ -1,7 +1,7 @@
GEM
remote: https://rubygems.org/
specs:
- rack (2.0.4)
+ rack (2.0.6)
rake (12.3.0)
PLATFORMS
@@ -12,4 +12,4 @@ DEPENDENCIES
rake
BUNDLED WITH
- 1.16.1
+ 1.17.1
diff --git a/qa/qa/git/repository.rb b/qa/qa/git/repository.rb
index 14cb8125fdb..7f959441dac 100644
--- a/qa/qa/git/repository.rb
+++ b/qa/qa/git/repository.rb
@@ -1,14 +1,24 @@
+# frozen_string_literal: true
+
require 'cgi'
require 'uri'
require 'open3'
+require 'fileutils'
+require 'tmpdir'
module QA
module Git
class Repository
include Scenario::Actable
+ attr_writer :password
+ attr_accessor :env_vars
+
def initialize
- @ssh_cmd = ""
+ # We set HOME to the current working directory (which is a
+ # temporary directory created in .perform()) so the temporarily dropped
+ # .netrc can be utilised
+ self.env_vars = [%Q{HOME="#{File.dirname(netrc_file_path)}"}]
end
def self.perform(*args)
@@ -21,36 +31,27 @@ module QA
@uri = URI(address)
end
- def username=(name)
- @username = name
- @uri.user = name
- end
-
- def password=(pass)
- @password = pass
- @uri.password = CGI.escape(pass).gsub('+', '%20')
+ def username=(username)
+ @username = username
+ @uri.user = username
end
def use_default_credentials
- if ::QA::Runtime::User.ldap_user?
- self.username = Runtime::User.ldap_username
- self.password = Runtime::User.ldap_password
- else
- self.username = Runtime::User.username
- self.password = Runtime::User.password
- end
+ self.username, self.password = default_credentials
+
+ add_credentials_to_netrc unless ssh_key_set?
end
def clone(opts = '')
- run_and_redact_credentials(build_git_command("git clone #{opts} #{@uri} ./"))
+ run("git clone #{opts} #{uri} ./")
end
def checkout(branch_name)
- `git checkout "#{branch_name}"`
+ run(%Q{git checkout "#{branch_name}"})
end
def checkout_new_branch(branch_name)
- `git checkout -b "#{branch_name}"`
+ run(%Q{git checkout -b "#{branch_name}"})
end
def shallow_clone
@@ -58,12 +59,10 @@ module QA
end
def configure_identity(name, email)
- `git config user.name #{name}`
- `git config user.email #{email}`
- end
+ run(%Q{git config user.name #{name}})
+ run(%Q{git config user.email #{email}})
- def configure_ssh_command(command)
- @ssh_cmd = "GIT_SSH_COMMAND='#{command}'"
+ add_credentials_to_netrc
end
def commit_file(name, contents, message)
@@ -74,54 +73,121 @@ module QA
def add_file(name, contents)
::File.write(name, contents)
- `git add #{name}`
+ run(%Q{git add #{name}})
end
def commit(message)
- `git commit -m "#{message}"`
+ run(%Q{git commit -m "#{message}"})
end
def push_changes(branch = 'master')
- output, _ = run_and_redact_credentials(build_git_command("git push #{@uri} #{branch}"))
-
- output
+ run("git push #{uri} #{branch}")
end
def commits
- `git log --oneline`.split("\n")
+ run('git log --oneline').split("\n")
end
def use_ssh_key(key)
@private_key_file = Tempfile.new("id_#{SecureRandom.hex(8)}")
- File.binwrite(@private_key_file, key.private_key)
- File.chmod(0700, @private_key_file)
+ File.binwrite(private_key_file, key.private_key)
+ File.chmod(0700, private_key_file)
@known_hosts_file = Tempfile.new("known_hosts_#{SecureRandom.hex(8)}")
keyscan_params = ['-H']
- keyscan_params << "-p #{@uri.port}" if @uri.port
- keyscan_params << @uri.host
- run_and_redact_credentials("ssh-keyscan #{keyscan_params.join(' ')} >> #{@known_hosts_file.path}")
+ keyscan_params << "-p #{uri.port}" if uri.port
+ keyscan_params << uri.host
+ run("ssh-keyscan #{keyscan_params.join(' ')} >> #{known_hosts_file.path}")
- configure_ssh_command("ssh -i #{@private_key_file.path} -o UserKnownHostsFile=#{@known_hosts_file.path}")
+ self.env_vars << %Q{GIT_SSH_COMMAND="ssh -i #{private_key_file.path} -o UserKnownHostsFile=#{known_hosts_file.path}"}
end
def delete_ssh_key
- return unless @private_key_file
+ return unless ssh_key_set?
+
+ private_key_file.close(true)
+ known_hosts_file.close(true)
+ end
+
+ def push_with_git_protocol(version, file_name, file_content, commit_message = 'Initial commit')
+ self.git_protocol = version
+ add_file(file_name, file_content)
+ commit(commit_message)
+ push_changes
+
+ fetch_supported_git_protocol
+ end
+
+ def git_protocol=(value)
+ raise ArgumentError, "Please specify the protocol you would like to use: 0, 1, or 2" unless %w[0 1 2].include?(value.to_s)
- @private_key_file.close(true)
- @known_hosts_file.close(true)
+ run("git config protocol.version #{value}")
end
- def build_git_command(command_str)
- [@ssh_cmd, command_str].compact.join(' ')
+ def fetch_supported_git_protocol
+ # ls-remote is one command known to respond to Git protocol v2 so we use
+ # it to get output including the version reported via Git tracing
+ output = run("git ls-remote #{uri}", "GIT_TRACE_PACKET=1")
+ output[/git< version (\d+)/, 1] || 'unknown'
end
private
- # Since the remote URL contains the credentials, and git occasionally
- # outputs the URL. Note that stderr is redirected to stdout.
- def run_and_redact_credentials(command)
- Open3.capture2("#{command} 2>&1 | sed -E 's#://[^@]+@#://****@#g'")
+ attr_reader :uri, :username, :password, :known_hosts_file, :private_key_file
+
+ def ssh_key_set?
+ !private_key_file.nil?
+ end
+
+ def run(command_str, *extra_env)
+ command = [env_vars, *extra_env, command_str, '2>&1'].compact.join(' ')
+ Runtime::Logger.debug "Git: command=[#{command}]"
+
+ output, _ = Open3.capture2(command)
+ output = output.chomp.gsub(/\s+$/, '')
+ Runtime::Logger.debug "Git: output=[#{output}]"
+
+ output
+ end
+
+ def default_credentials
+ if ::QA::Runtime::User.ldap_user?
+ [Runtime::User.ldap_username, Runtime::User.ldap_password]
+ else
+ [Runtime::User.username, Runtime::User.password]
+ end
+ end
+
+ def tmp_netrc_directory
+ @tmp_netrc_directory ||= File.join(Dir.tmpdir, "qa-netrc-credentials", $$.to_s)
+ end
+
+ def netrc_file_path
+ @netrc_file_path ||= File.join(tmp_netrc_directory, '.netrc')
+ end
+
+ def netrc_content
+ "machine #{uri.host} login #{username} password #{password}"
+ end
+
+ def netrc_already_contains_content?
+ File.exist?(netrc_file_path) &&
+ File.readlines(netrc_file_path).grep(/^#{netrc_content}$/).any?
+ end
+
+ def add_credentials_to_netrc
+ # Despite libcurl supporting a custom .netrc location through the
+ # CURLOPT_NETRC_FILE environment variable, git does not support it :(
+ # Info: https://curl.haxx.se/libcurl/c/CURLOPT_NETRC_FILE.html
+ #
+ # This will create a .netrc in the correct working directory, which is
+ # a temporary directory created in .perform()
+ #
+ return if netrc_already_contains_content?
+
+ FileUtils.mkdir_p(tmp_netrc_directory)
+ File.open(netrc_file_path, 'a') { |file| file.puts(netrc_content) }
+ File.chmod(0600, netrc_file_path)
end
end
end
diff --git a/qa/qa/page/README.md b/qa/qa/page/README.md
index 2dbc59846e7..d0de33892c4 100644
--- a/qa/qa/page/README.md
+++ b/qa/qa/page/README.md
@@ -70,15 +70,15 @@ module Page
module Main
class Login < Page::Base
view 'app/views/devise/passwords/edit.html.haml' do
- element :password_field, 'password_field :password'
- element :password_confirmation, 'password_field :password_confirmation'
- element :change_password_button, 'submit "Change your password"'
+ element :password_field
+ element :password_confirmation
+ element :change_password_button
end
view 'app/views/devise/sessions/_new_base.html.haml' do
- element :login_field, 'text_field :login'
- element :password_field, 'password_field :password'
- element :sign_in_button, 'submit "Sign in"'
+ element :login_field
+ element :password_field
+ element :sign_in_button
end
# ...
@@ -86,20 +86,32 @@ module Page
end
```
-It is possible to use `element` DSL method without value, with a String value
-or with a Regexp.
+The `view` DSL method declares the filename of the view where an
+`element` is implemented.
+
+The `element` DSL method in turn declares an element for which a corresponding
+`qa-element-name-dasherized` CSS class need to be added to the view file.
+
+You can also define a value (String or Regexp) to match to the actual view
+code but **this is deprecated** in favor of the above method for two reasons:
+
+- Consistency: there is only one way to define an element
+- Separation of concerns: QA uses dedicated CSS classes instead of reusing code
+ or classes used by other components (e.g. `js-*` classes etc.)
```ruby
view 'app/views/my/view.html.haml' do
+ # Implicitly require `.qa-logout-button` CSS class to be present in the view
+ element :logout_button
+
+ ## This is deprecated and forbidden by the `QA/ElementWithPattern` RuboCop cop.
# Require `f.submit "Sign in"` to be present in `my/view.html.haml
- element :my_button, 'f.submit "Sign in"'
+ element :my_button, 'f.submit "Sign in"' # rubocop:disable QA/ElementWithPattern
+ ## This is deprecated and forbidden by the `QA/ElementWithPattern` RuboCop cop.
# Match every line in `my/view.html.haml` against
# `/link_to .* "My Profile"/` regexp.
- element :profile_link, /link_to .* "My Profile"/
-
- # Implicitly require `.qa-logout-button` CSS class to be present in the view
- element :logout_button
+ element :profile_link, /link_to .* "My Profile"/ # rubocop:disable QA/ElementWithPattern
end
```
@@ -119,4 +131,4 @@ If you need more information, ask for help on `#quality` channel on Slack
(internal, GitLab Team only).
If you are not a Team Member, and you still need help to contribute, please
-open an issue in GitLab QA issue tracker.
+open an issue in GitLab CE issue tracker with the `~QA` label.
diff --git a/qa/qa/page/admin/menu.rb b/qa/qa/page/admin/menu.rb
new file mode 100644
index 00000000000..e8c7d274966
--- /dev/null
+++ b/qa/qa/page/admin/menu.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+module QA
+ module Page
+ module Admin
+ class Menu < Page::Base
+ view 'app/views/layouts/nav/sidebar/_admin.html.haml' do
+ element :admin_sidebar
+ element :admin_sidebar_submenu
+ element :admin_settings_item
+ element :admin_settings_repository_item
+ end
+
+ def go_to_repository_settings
+ hover_settings do
+ within_submenu do
+ click_element :admin_settings_repository_item
+ end
+ end
+ end
+
+ private
+
+ def hover_settings
+ within_sidebar do
+ scroll_to_element(:admin_settings_item)
+ find_element(:admin_settings_item).hover
+
+ yield
+ end
+ end
+
+ def within_sidebar
+ within_element(:admin_sidebar) do
+ yield
+ end
+ end
+
+ def within_submenu
+ within_element(:admin_sidebar_submenu) do
+ yield
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/admin/settings/component/repository_storage.rb b/qa/qa/page/admin/settings/component/repository_storage.rb
new file mode 100644
index 00000000000..2ed0cd73aa3
--- /dev/null
+++ b/qa/qa/page/admin/settings/component/repository_storage.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module QA
+ module Page
+ module Admin
+ module Settings
+ module Component
+ class RepositoryStorage < Page::Base
+ view 'app/views/admin/application_settings/_repository_storage.html.haml' do
+ element :hashed_storage_checkbox
+ element :save_changes_button
+ end
+
+ def enable_hashed_storage
+ check_element :hashed_storage_checkbox
+ end
+
+ def save_settings
+ click_element :save_changes_button
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/admin/settings/main.rb b/qa/qa/page/admin/settings/main.rb
deleted file mode 100644
index 73034ffe0d8..00000000000
--- a/qa/qa/page/admin/settings/main.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-module QA
- module Page
- module Admin
- module Settings
- class Main < Page::Base
- include QA::Page::Settings::Common
-
- view 'app/views/admin/application_settings/show.html.haml' do
- element :terms_settings
- end
-
- def expand_repository_storage(&block)
- expand_section(:terms_settings) do
- RepositoryStorage.perform(&block)
- end
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/page/admin/settings/repository.rb b/qa/qa/page/admin/settings/repository.rb
new file mode 100644
index 00000000000..b7f1deb21bd
--- /dev/null
+++ b/qa/qa/page/admin/settings/repository.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module QA
+ module Page
+ module Admin
+ module Settings
+ class Repository < Page::Base
+ include QA::Page::Settings::Common
+
+ view 'app/views/admin/application_settings/repository.html.haml' do
+ element :repository_storage_settings
+ end
+
+ def expand_repository_storage(&block)
+ expand_section(:repository_storage_settings) do
+ Component::RepositoryStorage.perform(&block)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/admin/settings/repository_storage.rb b/qa/qa/page/admin/settings/repository_storage.rb
deleted file mode 100644
index 68dd23a41e1..00000000000
--- a/qa/qa/page/admin/settings/repository_storage.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-module QA
- module Page
- module Admin
- module Settings
- class RepositoryStorage < Page::Base
- view 'app/views/admin/application_settings/_repository_storage.html.haml' do
- element :submit, "submit 'Save changes'"
- element :hashed_storage,
- 'Use hashed storage paths for newly created and renamed projects'
- end
-
- def enable_hashed_storage
- check 'Use hashed storage paths for newly created and renamed projects'
- end
-
- def save_settings
- click_button 'Save changes'
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/page/base.rb b/qa/qa/page/base.rb
index 30e35bf7abb..91e229c4c8c 100644
--- a/qa/qa/page/base.rb
+++ b/qa/qa/page/base.rb
@@ -1,12 +1,17 @@
+# frozen_string_literal: true
+
require 'capybara/dsl'
module QA
module Page
class Base
+ prepend Support::Page::Logging if Runtime::Env.debug?
include Capybara::DSL
include Scenario::Actable
extend SingleForwardable
+ ElementNotFound = Class.new(RuntimeError)
+
def_delegators :evaluator, :view, :views
def refresh
@@ -28,6 +33,21 @@ module QA
false
end
+ def with_retry(max_attempts: 3, reload: false)
+ attempts = 0
+
+ while attempts < max_attempts
+ result = yield
+ return result if result
+
+ refresh if reload
+
+ attempts += 1
+ end
+
+ false
+ end
+
def scroll_to(selector, text: nil)
page.execute_script <<~JS
var elements = Array.from(document.querySelectorAll('#{selector}'));
@@ -68,6 +88,10 @@ module QA
all(element_selector_css(name))
end
+ def check_element(name)
+ find_element(name).set(true)
+ end
+
def click_element(name)
find_element(name).click
end
@@ -76,12 +100,20 @@ module QA
find_element(name).set(content)
end
+ def has_element?(name)
+ has_css?(element_selector_css(name))
+ end
+
def within_element(name)
page.within(element_selector_css(name)) do
yield
end
end
+ def scroll_to_element(name, *args)
+ scroll_to(element_selector_css(name), *args)
+ end
+
def element_selector_css(name)
Page::Element.new(name).selector_css
end
diff --git a/qa/qa/page/component/clone_panel.rb b/qa/qa/page/component/clone_panel.rb
index 8e8ff4e3bb0..94e761b0e0c 100644
--- a/qa/qa/page/component/clone_panel.rb
+++ b/qa/qa/page/component/clone_panel.rb
@@ -7,8 +7,8 @@ module QA
def self.included(base)
base.view 'app/views/shared/_clone_panel.html.haml' do
element :clone_dropdown
- element :clone_options_dropdown, '.clone-options-dropdown'
- element :project_repository_location, 'text_field_tag :project_clone'
+ element :clone_options_dropdown, '.clone-options-dropdown' # rubocop:disable QA/ElementWithPattern
+ element :project_repository_location, 'text_field_tag :project_clone' # rubocop:disable QA/ElementWithPattern
end
end
diff --git a/qa/qa/page/component/dropdown_filter.rb b/qa/qa/page/component/dropdown_filter.rb
new file mode 100644
index 00000000000..e896c382779
--- /dev/null
+++ b/qa/qa/page/component/dropdown_filter.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module QA
+ module Page
+ module Component
+ module DropdownFilter
+ def filter_and_select(item)
+ wait(reload: false) do
+ page.has_css?('.dropdown-input-field')
+ end
+
+ find('.dropdown-input-field').set(item)
+ click_link item
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/component/groups_filter.rb b/qa/qa/page/component/groups_filter.rb
index e647d368f0f..cc50bb439b4 100644
--- a/qa/qa/page/component/groups_filter.rb
+++ b/qa/qa/page/component/groups_filter.rb
@@ -6,12 +6,7 @@ module QA
module GroupsFilter
def self.included(base)
base.view 'app/views/shared/groups/_search_form.html.haml' do
- element :groups_filter, 'search_field_tag :filter'
- element :groups_filter_placeholder, 'Search by name'
- end
-
- base.view 'app/views/shared/groups/_empty_state.html.haml' do
- element :groups_empty_state
+ element :groups_filter
end
base.view 'app/assets/javascripts/groups/components/groups.vue' do
@@ -21,13 +16,22 @@ module QA
private
- def filter_by_name(name)
+ def has_filtered_group?(name)
+ # Filter and submit to reload the page and only retrieve the filtered results
+ find_element(:groups_filter).set(name).send_keys(:return)
+
+ # 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_empty_state)) ||
- page.has_css?(element_selector_css(:groups_list_tree_container))
+ page.has_css?(element_selector_css(:groups_list_tree_container))
end
- fill_in 'Search by name', with: name
+ # 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)
+
+ # The name will be present as filter input so we check for a link, not text
+ page.has_link?(name, wait: 0)
end
end
end
diff --git a/qa/qa/page/component/issuable/common.rb b/qa/qa/page/component/issuable/common.rb
new file mode 100644
index 00000000000..cfd8ac1e7c8
--- /dev/null
+++ b/qa/qa/page/component/issuable/common.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module QA
+ module Page
+ module Component
+ module Issuable
+ module Common
+ def self.included(base)
+ base.view 'app/assets/javascripts/issue_show/components/title.vue' do
+ element :edit_button
+ end
+
+ base.view 'app/assets/javascripts/issue_show/components/fields/title.vue' do
+ element :title_input
+ end
+
+ base.view 'app/assets/javascripts/issue_show/components/fields/description.vue' do
+ element :description_textarea
+ end
+
+ base.view 'app/assets/javascripts/issue_show/components/edit_actions.vue' do
+ 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
+ end
+ end
+end
diff --git a/qa/qa/page/component/select2.rb b/qa/qa/page/component/select2.rb
index 30829eb0221..6d07d5a10e6 100644
--- a/qa/qa/page/component/select2.rb
+++ b/qa/qa/page/component/select2.rb
@@ -3,7 +3,12 @@ module QA
module Component
module Select2
def select_item(item_text)
- find('ul.select2-result-sub > li', text: item_text).click
+ find('.select2-result-label', text: item_text).click
+ end
+
+ def search_and_select(item_text)
+ find('.select2-input').set(item_text)
+ select_item(item_text)
end
end
end
diff --git a/qa/qa/page/component/users_select.rb b/qa/qa/page/component/users_select.rb
new file mode 100644
index 00000000000..f88d6450a33
--- /dev/null
+++ b/qa/qa/page/component/users_select.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module QA
+ module Page
+ module Component
+ module UsersSelect
+ def select_user(element, username)
+ find("#{element_selector_css(element)} input").set(username)
+ find('.ajax-users-dropdown .user-username', text: "@#{username}").click
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/dashboard/groups.rb b/qa/qa/page/dashboard/groups.rb
index 70c5f996ff8..7a07515de62 100644
--- a/qa/qa/page/dashboard/groups.rb
+++ b/qa/qa/page/dashboard/groups.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module QA
module Page
module Dashboard
@@ -5,18 +7,16 @@ module QA
include Page::Component::GroupsFilter
view 'app/views/shared/groups/_search_form.html.haml' do
- element :groups_filter, 'search_field_tag :filter'
- element :groups_filter_placeholder, 'Search by name'
+ element :groups_filter, 'search_field_tag :filter' # rubocop:disable QA/ElementWithPattern
+ element :groups_filter_placeholder, 'Search by name' # rubocop:disable QA/ElementWithPattern
end
view 'app/views/dashboard/_groups_head.html.haml' do
- element :new_group_button, 'link_to _("New group")'
+ element :new_group_button, 'link_to _("New group")' # rubocop:disable QA/ElementWithPattern
end
def has_group?(name)
- filter_by_name(name)
-
- page.has_link?(name)
+ has_filtered_group?(name)
end
def go_to_group(name)
diff --git a/qa/qa/page/dashboard/projects.rb b/qa/qa/page/dashboard/projects.rb
index 5b2827c089c..0f434577b3b 100644
--- a/qa/qa/page/dashboard/projects.rb
+++ b/qa/qa/page/dashboard/projects.rb
@@ -5,7 +5,7 @@ module QA
view 'app/views/dashboard/projects/index.html.haml'
view 'app/views/shared/projects/_search_form.html.haml' do
- element :form_filter_by_name, /form_tag.+id: 'project-filter-form'/
+ element :form_filter_by_name, /form_tag.+id: 'project-filter-form'/ # rubocop:disable QA/ElementWithPattern
end
def go_to_project(name)
diff --git a/qa/qa/page/file/form.rb b/qa/qa/page/file/form.rb
index f6e502f500b..a1534231691 100644
--- a/qa/qa/page/file/form.rb
+++ b/qa/qa/page/file/form.rb
@@ -3,14 +3,23 @@ module QA
module File
class Form < Page::Base
include Shared::CommitMessage
+ include Page::Component::DropdownFilter
view 'app/views/projects/blob/_editor.html.haml' do
- element :file_name, "text_field_tag 'file_name'"
- element :editor, '#editor'
+ element :file_name, "text_field_tag 'file_name'" # rubocop:disable QA/ElementWithPattern
+ element :editor, '#editor' # rubocop:disable QA/ElementWithPattern
end
view 'app/views/projects/_commit_button.html.haml' do
- element :commit_changes, "button_tag 'Commit changes'"
+ element :commit_changes, "button_tag 'Commit changes'" # rubocop:disable QA/ElementWithPattern
+ end
+
+ view 'app/views/projects/blob/_template_selectors.html.haml' do
+ element :template_type_dropdown
+ element :gitignore_dropdown
+ element :gitlab_ci_yml_dropdown
+ element :dockerfile_dropdown
+ element :license_dropdown
end
def add_name(name)
@@ -29,6 +38,25 @@ module QA
click_on 'Commit changes'
end
+ def select_template(template_type, template)
+ click_element :template_type_dropdown
+ click_link template_type
+
+ case template_type
+ when '.gitignore'
+ click_element :gitignore_dropdown
+ when '.gitlab-ci.yml'
+ click_element :gitlab_ci_yml_dropdown
+ when 'Dockerfile'
+ click_element :dockerfile_dropdown
+ when 'LICENSE'
+ click_element :license_dropdown
+ else
+ raise %Q(Unsupported template_type "#{template_type}". Please confirm that it is a valid option.)
+ end
+ filter_and_select template
+ end
+
private
def text_area
diff --git a/qa/qa/page/file/shared/commit_message.rb b/qa/qa/page/file/shared/commit_message.rb
index 5af1a55e2ef..aa1bb081cd3 100644
--- a/qa/qa/page/file/shared/commit_message.rb
+++ b/qa/qa/page/file/shared/commit_message.rb
@@ -5,7 +5,7 @@ module QA
module CommitMessage
def self.included(base)
base.view 'app/views/shared/_commit_message_container.html.haml' do
- element :commit_message, "text_area_tag 'commit_message'"
+ element :commit_message, "text_area_tag 'commit_message'" # rubocop:disable QA/ElementWithPattern
end
end
diff --git a/qa/qa/page/file/show.rb b/qa/qa/page/file/show.rb
index 99f5924b67f..abd8ebb089f 100644
--- a/qa/qa/page/file/show.rb
+++ b/qa/qa/page/file/show.rb
@@ -5,12 +5,12 @@ module QA
include Shared::CommitMessage
view 'app/helpers/blob_helper.rb' do
- element :edit_button, "_('Edit')"
- element :delete_button, /label:\s+"Delete"/
+ element :edit_button, "_('Edit')" # rubocop:disable QA/ElementWithPattern
+ element :delete_button, /label:\s+"Delete"/ # rubocop:disable QA/ElementWithPattern
end
view 'app/views/projects/blob/_remove.html.haml' do
- element :delete_file_button, "button_tag 'Delete file'"
+ element :delete_file_button, "button_tag 'Delete file'" # rubocop:disable QA/ElementWithPattern
end
def click_edit
diff --git a/qa/qa/page/group/new.rb b/qa/qa/page/group/new.rb
index 48b71a7c883..39584daf334 100644
--- a/qa/qa/page/group/new.rb
+++ b/qa/qa/page/group/new.rb
@@ -3,14 +3,14 @@ module QA
module Group
class New < Page::Base
view 'app/views/shared/_group_form.html.haml' do
- element :group_path_field, 'text_field :path'
- element :group_name_field, 'text_field :name'
- element :group_description_field, 'text_area :description'
+ element :group_path_field, 'text_field :path' # rubocop:disable QA/ElementWithPattern
+ element :group_name_field, 'text_field :name' # rubocop:disable QA/ElementWithPattern
+ element :group_description_field, 'text_area :description' # rubocop:disable QA/ElementWithPattern
end
view 'app/views/groups/new.html.haml' do
- element :create_group_button, "submit 'Create group'"
- element :visibility_radios, 'visibility_level:'
+ element :create_group_button, "submit 'Create group'" # rubocop:disable QA/ElementWithPattern
+ element :visibility_radios, 'visibility_level:' # rubocop:disable QA/ElementWithPattern
end
def set_path(path)
diff --git a/qa/qa/page/group/show.rb b/qa/qa/page/group/show.rb
index 6747f7f10b6..0f0ab81a4ef 100644
--- a/qa/qa/page/group/show.rb
+++ b/qa/qa/page/group/show.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module QA
module Page
module Group
@@ -5,18 +7,15 @@ module QA
include Page::Component::GroupsFilter
view 'app/views/groups/show.html.haml' do
- element :new_project_or_subgroup_dropdown, '.new-project-subgroup'
- element :new_project_or_subgroup_dropdown_toggle, '.dropdown-toggle'
- element :new_project_option, /%li.*data:.*value: "new-project"/
- element :new_project_button, /%input.*data:.*action: "new-project"/
- element :new_subgroup_option, /%li.*data:.*value: "new-subgroup"/
-
- # data-value and data-action get modified by JS for subgroup
- element :new_subgroup_button, /%input.*\.js-new-group-child/
+ element :new_project_or_subgroup_dropdown
+ element :new_project_or_subgroup_dropdown_toggle
+ element :new_project_option
+ element :new_subgroup_option
+ element :new_in_group_button
end
view 'app/assets/javascripts/groups/constants.js' do
- element :no_result_text, 'No groups or projects matched your search'
+ element :no_result_text, 'No groups or projects matched your search' # rubocop:disable QA/ElementWithPattern
end
def go_to_subgroup(name)
@@ -24,43 +23,37 @@ module QA
end
def has_new_project_or_subgroup_dropdown?
- page.has_css?(element_selector_css(:new_project_or_subgroup_dropdown))
+ has_element?(:new_project_or_subgroup_dropdown)
end
def has_subgroup?(name)
- filter_by_name(name)
-
- page.has_text?(/#{name}|No groups or projects matched your search/, wait: 60)
-
- page.has_text?(name, wait: 0)
+ has_filtered_group?(name)
end
def go_to_new_subgroup
- click_new('subgroup')
+ select_kind :new_subgroup_option
- find("input[data-action='new-subgroup']").click
+ click_element :new_in_group_button
end
def go_to_new_project
- click_new('project')
+ select_kind :new_project_option
- find("input[data-action='new-project']").click
+ click_element :new_in_group_button
end
private
- def click_new(kind)
- within '.new-project-subgroup' do
- css = "li[data-value='new-#{kind}']"
-
+ def select_kind(kind)
+ 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
- find('.dropdown-toggle').click
+ click_element :new_project_or_subgroup_dropdown_toggle
- page.has_css?(css)
+ has_element?(kind)
end
- find(css).click
+ click_element kind
end
end
end
diff --git a/qa/qa/page/issuable/sidebar.rb b/qa/qa/page/issuable/sidebar.rb
index f207264e24f..d3751b712c9 100644
--- a/qa/qa/page/issuable/sidebar.rb
+++ b/qa/qa/page/issuable/sidebar.rb
@@ -3,8 +3,8 @@ module QA
module Issuable
class Sidebar < Page::Base
view 'app/views/shared/issuable/_sidebar.html.haml' do
- element :labels_block, ".issuable-show-labels"
- element :milestones_block, '.block.milestone'
+ element :labels_block, ".issuable-show-labels" # rubocop:disable QA/ElementWithPattern
+ element :milestones_block, '.block.milestone' # rubocop:disable QA/ElementWithPattern
end
def has_label?(label)
diff --git a/qa/qa/page/label/index.rb b/qa/qa/page/label/index.rb
new file mode 100644
index 00000000000..323acd57743
--- /dev/null
+++ b/qa/qa/page/label/index.rb
@@ -0,0 +1,15 @@
+module QA
+ module Page
+ module Label
+ class Index < Page::Base
+ view 'app/views/projects/labels/index.html.haml' do
+ element :label_create_new
+ end
+
+ def go_to_new_label
+ click_element :label_create_new
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/label/new.rb b/qa/qa/page/label/new.rb
new file mode 100644
index 00000000000..b5422dc9400
--- /dev/null
+++ b/qa/qa/page/label/new.rb
@@ -0,0 +1,30 @@
+module QA
+ module Page
+ module Label
+ class New < Page::Base
+ view 'app/views/shared/labels/_form.html.haml' do
+ element :label_title
+ element :label_description
+ element :label_color
+ element :label_create_button
+ end
+
+ def create_label
+ click_element :label_create_button
+ end
+
+ def fill_title(title)
+ fill_element :label_title, title
+ end
+
+ def fill_description(description)
+ fill_element :label_description, description
+ end
+
+ def fill_color(color)
+ fill_element :label_color, color
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/layout/banner.rb b/qa/qa/page/layout/banner.rb
index e7654bdafc9..2223f2adec8 100644
--- a/qa/qa/page/layout/banner.rb
+++ b/qa/qa/page/layout/banner.rb
@@ -3,7 +3,7 @@ module QA
module Layout
class Banner < Page::Base
view 'app/views/layouts/header/_read_only_banner.html.haml' do
- element :flash_notice, ".flash-notice"
+ element :flash_notice, ".flash-notice" # rubocop:disable QA/ElementWithPattern
end
def has_notice?(message)
diff --git a/qa/qa/page/main/login.rb b/qa/qa/page/main/login.rb
index e9e49964e63..97ffe0e5716 100644
--- a/qa/qa/page/main/login.rb
+++ b/qa/qa/page/main/login.rb
@@ -23,6 +23,7 @@ module QA
view 'app/views/devise/shared/_tabs_ldap.html.haml' do
element :ldap_tab
element :standard_tab
+ element :register_tab
end
view 'app/views/devise/shared/_tabs_normal.html.haml' do
@@ -30,19 +31,23 @@ module QA
element :register_tab
end
+ view 'app/views/devise/shared/_omniauth_box.html.haml' do
+ element :saml_login_button
+ end
+
def initialize
# The login page is usually the entry point for all the scenarios so
# we need to wait for the instance to start. That said, in some cases
# we are already logged-in so we check both cases here.
wait(max: 500) do
- page.has_css?('.login-page') ||
- Page::Menu::Main.act { has_personal_area?(wait: 0) }
+ has_css?('.login-page') ||
+ Page::Main::Menu.act { has_personal_area?(wait: 0) }
end
end
def sign_in_using_credentials(user = nil)
# Don't try to log-in if we're already logged-in
- return if Page::Menu::Main.act { has_personal_area?(wait: 0) }
+ return if Page::Main::Menu.act { has_personal_area?(wait: 0) }
using_wait_time 0 do
set_initial_password_if_present
@@ -56,11 +61,11 @@ module QA
end
end
- Page::Menu::Main.act { has_personal_area? }
+ Page::Main::Menu.act { has_personal_area? }
end
def sign_in_using_admin_credentials
- admin = QA::Factory::Resource::User.new.tap do |user|
+ admin = QA::Resource::User.new.tap do |user|
user.username = QA::Runtime::User.admin_username
user.password = QA::Runtime::User.admin_password
end
@@ -71,19 +76,35 @@ module QA
sign_in_using_gitlab_credentials(admin)
end
- Page::Menu::Main.act { has_personal_area? }
+ Page::Main::Menu.act { has_personal_area? }
end
def self.path
'/users/sign_in'
end
+ def has_sign_in_tab?
+ has_element?(:sign_in_tab)
+ end
+
+ def has_ldap_tab?
+ has_element?(:ldap_tab)
+ end
+
+ def has_standard_tab?
+ has_element?(:standard_tab)
+ end
+
def sign_in_tab?
- page.has_button?('Sign in')
+ has_css?(".active", text: 'Sign in')
end
def ldap_tab?
- page.has_link?('LDAP')
+ has_css?(".active", text: 'LDAP')
+ end
+
+ def standard_tab?
+ has_css?(".active", text: 'Standard')
end
def switch_to_sign_in_tab
@@ -91,6 +112,7 @@ module QA
end
def switch_to_register_tab
+ set_initial_password_if_present
click_element :register_tab
end
@@ -112,9 +134,14 @@ module QA
click_element :sign_in_button
end
+ def sign_in_with_saml
+ set_initial_password_if_present
+ click_element :saml_login_button
+ end
+
def sign_in_using_gitlab_credentials(user)
- switch_to_sign_in_tab unless sign_in_tab?
- switch_to_standard_tab if ldap_tab?
+ switch_to_sign_in_tab if has_sign_in_tab?
+ switch_to_standard_tab if has_standard_tab?
fill_element :login_field, user.username
fill_element :password_field, user.password
@@ -122,7 +149,7 @@ module QA
end
def set_initial_password_if_present
- return unless page.has_content?('Change your password')
+ return unless has_content?('Change your password')
fill_element :password_field, Runtime::User.password
fill_element :password_confirmation, Runtime::User.password
diff --git a/qa/qa/page/menu/main.rb b/qa/qa/page/main/menu.rb
index 36e7285f7b7..cc2724618e9 100644
--- a/qa/qa/page/menu/main.rb
+++ b/qa/qa/page/main/menu.rb
@@ -1,16 +1,18 @@
+# frozen_string_literal: true
+
module QA
module Page
- module Menu
- class Main < Page::Base
+ module Main
+ class Menu < Page::Base
view 'app/views/layouts/header/_current_user_dropdown.html.haml' do
- element :user_sign_out_link, 'link_to _("Sign out")'
- element :settings_link, 'link_to s_("CurrentUser|Settings")'
+ element :user_sign_out_link, 'link_to _("Sign out")' # rubocop:disable QA/ElementWithPattern
+ element :settings_link, 'link_to s_("CurrentUser|Settings")' # rubocop:disable QA/ElementWithPattern
end
view 'app/views/layouts/header/_default.html.haml' do
element :navbar
element :user_avatar
- element :user_menu, '.dropdown-menu'
+ element :user_menu, '.dropdown-menu' # rubocop:disable QA/ElementWithPattern
end
view 'app/views/layouts/nav/_dashboard.html.haml' do
@@ -61,8 +63,15 @@ module QA
end
def has_personal_area?(wait: Capybara.default_max_wait_time)
- # No need to wait, either we're logged-in, or not.
- using_wait_time(wait) { page.has_selector?('.qa-user-avatar') }
+ using_wait_time(wait) do
+ page.has_selector?(element_selector_css(:user_avatar))
+ end
+ end
+
+ def has_admin_area_link?(wait: Capybara.default_max_wait_time)
+ using_wait_time(wait) do
+ page.has_selector?(element_selector_css(:admin_area_link))
+ end
end
private
diff --git a/qa/qa/page/main/oauth.rb b/qa/qa/page/main/oauth.rb
index 618f114e058..bc44d274314 100644
--- a/qa/qa/page/main/oauth.rb
+++ b/qa/qa/page/main/oauth.rb
@@ -3,7 +3,7 @@ module QA
module Main
class OAuth < Page::Base
view 'app/views/doorkeeper/authorizations/new.html.haml' do
- element :authorization_button, 'submit_tag _("Authorize")'
+ element :authorization_button, 'submit_tag _("Authorize")' # rubocop:disable QA/ElementWithPattern
end
def needs_authorization?
diff --git a/qa/qa/page/main/sign_up.rb b/qa/qa/page/main/sign_up.rb
index 33ab56236f4..9ca498012eb 100644
--- a/qa/qa/page/main/sign_up.rb
+++ b/qa/qa/page/main/sign_up.rb
@@ -1,25 +1,35 @@
+# frozen_string_literal: true
+
module QA
module Page
module Main
class SignUp < Page::Base
view 'app/views/devise/shared/_signup_box.html.haml' do
- element :name, 'text_field :name'
- element :username, 'text_field :username'
- element :email_field, 'email_field :email'
- element :email_confirmation, 'email_field :email_confirmation'
- element :password, 'password_field :password'
- element :register_button, 'submit "Register"'
+ element :new_user_name
+ element :new_user_username
+ element :new_user_email
+ element :new_user_email_confirmation
+ element :new_user_password
+ element :new_user_register_button
+ element :new_user_accept_terms
end
def sign_up!(user)
- fill_in :new_user_name, with: user.name
- fill_in :new_user_username, with: user.username
- fill_in :new_user_email, with: user.email
- fill_in :new_user_email_confirmation, with: user.email
- fill_in :new_user_password, with: user.password
- click_button 'Register'
+ fill_element :new_user_name, user.name
+ fill_element :new_user_username, user.username
+ fill_element :new_user_email, user.email
+ fill_element :new_user_email_confirmation, user.email
+ fill_element :new_user_password, user.password
+
+ check_element :new_user_accept_terms if has_element?(:new_user_accept_terms)
+
+ signed_in = with_retry do
+ click_element :new_user_register_button
+
+ Page::Main::Menu.act { has_personal_area? }
+ end
- Page::Menu::Main.act { has_personal_area? }
+ raise "Failed to register and sign in" unless signed_in
end
end
end
diff --git a/qa/qa/page/menu/admin.rb b/qa/qa/page/menu/admin.rb
deleted file mode 100644
index 573b98f7386..00000000000
--- a/qa/qa/page/menu/admin.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-module QA
- module Page
- module Menu
- class Admin < Page::Base
- view 'app/views/layouts/nav/sidebar/_admin.html.haml' do
- element :settings, "_('Settings')"
- end
-
- def go_to_settings
- click_link 'Settings'
- end
- end
- end
- end
-end
diff --git a/qa/qa/page/merge_request/new.rb b/qa/qa/page/merge_request/new.rb
index 83cc4bbbace..1f8f1fbca8e 100644
--- a/qa/qa/page/merge_request/new.rb
+++ b/qa/qa/page/merge_request/new.rb
@@ -22,6 +22,10 @@ module QA
element :issuable_dropdown_menu_milestone
end
+ view 'app/views/shared/issuable/_label_dropdown.html.haml' do
+ element :issuable_label
+ end
+
def create_merge_request
click_element :issuable_create_button
end
@@ -40,6 +44,12 @@ module QA
click_on milestone.title
end
end
+
+ def select_label(label)
+ click_element :issuable_label
+
+ click_link label.title
+ end
end
end
end
diff --git a/qa/qa/page/merge_request/show.rb b/qa/qa/page/merge_request/show.rb
index befb7c1809a..2fd30e15ffb 100644
--- a/qa/qa/page/merge_request/show.rb
+++ b/qa/qa/page/merge_request/show.rb
@@ -1,28 +1,64 @@
+# frozen_string_literal: true
+
module QA
module Page
module MergeRequest
class Show < Page::Base
view 'app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue' do
element :merge_button
- element :fast_forward_message, 'Fast-forward merge without a merge commit'
+ element :fast_forward_message, 'Fast-forward merge without a merge commit' # rubocop:disable QA/ElementWithPattern
element :merge_moment_dropdown
element :merge_when_pipeline_succeeds_option
element :merge_immediately_option
end
view 'app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue' do
- element :merged_status, 'The changes were merged into'
+ element :merged_status, 'The changes were merged into' # rubocop:disable QA/ElementWithPattern
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'
+ element :no_fast_forward_message, 'Fast-forward merge is not possible' # rubocop:disable QA/ElementWithPattern
end
- view 'app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.vue' do
+ view 'app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue' do
element :squash_checkbox
end
+ view 'app/views/projects/merge_requests/show.html.haml' do
+ element :notes_tab
+ element :diffs_tab
+ end
+
+ view 'app/assets/javascripts/diffs/components/diff_line_gutter_content.vue' do
+ element :diff_comment
+ end
+
+ view 'app/assets/javascripts/notes/components/comment_form.vue' do
+ element :note_dropdown
+ element :discussion_option
+ end
+
+ view 'app/assets/javascripts/notes/components/note_form.vue' do
+ element :reply_input
+ end
+
+ view 'app/assets/javascripts/notes/components/noteable_discussion.vue' do
+ element :discussion_reply
+ end
+
+ view 'app/assets/javascripts/diffs/components/inline_diff_table_row.vue' do
+ element :new_diff_line
+ end
+
+ view 'app/views/shared/issuable/_sidebar.html.haml' do
+ element :labels_block
+ end
+
+ view 'app/views/projects/merge_requests/_mr_title.html.haml' do
+ element :edit_button
+ end
+
def fast_forward_possible?
!has_text?('Fast-forward merge is not possible')
end
@@ -64,6 +100,13 @@ module QA
end
end
+ def has_label?(label)
+ page.within(element_selector_css(:labels_block)) do
+ element = find('span', text: label)
+ !element.nil?
+ end
+ end
+
def merge!
# The merge button is disabled on load
wait do
@@ -95,6 +138,39 @@ module QA
click_element :squash_checkbox
end
+
+ def go_to_discussions_tab
+ click_element :notes_tab
+ end
+
+ def go_to_diffs_tab
+ click_element :diffs_tab
+ end
+
+ def add_comment_to_diff(text)
+ wait(time: 5) do
+ page.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
+ end
+
+ def start_discussion(text)
+ fill_element :comment_input, text
+ click_element :note_dropdown
+ click_element :discussion_option
+ click_element :comment_button
+ end
+
+ def reply_to_discussion(reply_text)
+ all_elements(:discussion_reply).last.click
+ fill_element :reply_input, reply_text
+ end
+
+ def edit!
+ click_element :edit_button
+ end
end
end
end
diff --git a/qa/qa/page/menu/profile.rb b/qa/qa/page/profile/menu.rb
index 7e24fa85c33..2d503499e13 100644
--- a/qa/qa/page/menu/profile.rb
+++ b/qa/qa/page/profile/menu.rb
@@ -1,12 +1,14 @@
+# frozen_string_literal: true
+
module QA
module Page
- module Menu
- class Profile < Page::Base
+ module Profile
+ class Menu < Page::Base
view 'app/views/layouts/nav/sidebar/_profile.html.haml' do
- element :access_token_link, 'link_to profile_personal_access_tokens_path'
- element :access_token_title, 'Access Tokens'
- element :top_level_items, '.sidebar-top-level-items'
- element :ssh_keys, 'SSH Keys'
+ element :access_token_link, 'link_to profile_personal_access_tokens_path' # rubocop:disable QA/ElementWithPattern
+ element :access_token_title, 'Access Tokens' # rubocop:disable QA/ElementWithPattern
+ element :top_level_items, '.sidebar-top-level-items' # rubocop:disable QA/ElementWithPattern
+ element :ssh_keys, 'SSH Keys' # rubocop:disable QA/ElementWithPattern
end
def click_access_tokens
diff --git a/qa/qa/page/profile/personal_access_tokens.rb b/qa/qa/page/profile/personal_access_tokens.rb
index f5ae47dadd0..9191dbe9cf3 100644
--- a/qa/qa/page/profile/personal_access_tokens.rb
+++ b/qa/qa/page/profile/personal_access_tokens.rb
@@ -3,13 +3,13 @@ module QA
module Profile
class PersonalAccessTokens < Page::Base
view 'app/views/shared/_personal_access_tokens_form.html.haml' do
- element :personal_access_token_name_field, 'text_field :name'
- element :create_token_button, 'submit "Create #{type} token"' # rubocop:disable Lint/InterpolationCheck
- element :scopes_api_radios, "label :scopes"
+ element :personal_access_token_name_field, 'text_field :name' # rubocop:disable QA/ElementWithPattern
+ element :create_token_button, 'submit "Create #{type} token"' # rubocop:disable QA/ElementWithPattern, Lint/InterpolationCheck
+ element :scopes_api_radios, "label :scopes" # rubocop:disable QA/ElementWithPattern
end
- view 'app/views/profiles/personal_access_tokens/index.html.haml' do
- element :create_token_field, "text_field_tag 'created-personal-access-token'"
+ view 'app/views/shared/_personal_access_tokens_created_container.html.haml' do
+ element :create_token_field, "text_field_tag 'created-personal-access-token'" # rubocop:disable QA/ElementWithPattern
end
def fill_token_name(name)
diff --git a/qa/qa/page/project/activity.rb b/qa/qa/page/project/activity.rb
index 0196922c889..56fbaa90790 100644
--- a/qa/qa/page/project/activity.rb
+++ b/qa/qa/page/project/activity.rb
@@ -3,7 +3,7 @@ module QA
module Project
class Activity < Page::Base
view 'app/views/shared/_event_filter.html.haml' do
- element :push_events, "event_filter_link EventFilter.push, _('Push events')"
+ element :push_events, "event_filter_link EventFilter::PUSH, _('Push events')" # rubocop:disable QA/ElementWithPattern
end
def go_to_push_events
diff --git a/qa/qa/page/project/fork/new.rb b/qa/qa/page/project/fork/new.rb
index ed92df956bf..140c004b458 100644
--- a/qa/qa/page/project/fork/new.rb
+++ b/qa/qa/page/project/fork/new.rb
@@ -4,7 +4,7 @@ module QA
module Fork
class New < Page::Base
view 'app/views/projects/forks/_fork_button.html.haml' do
- element :namespace, 'link_to project_forks_path'
+ element :namespace, 'link_to project_forks_path' # rubocop:disable QA/ElementWithPattern
end
def choose_namespace(namespace = Runtime::Namespace.path)
diff --git a/qa/qa/page/project/import/github.rb b/qa/qa/page/project/import/github.rb
index 1a410a0f8a5..a3cde73d3f2 100644
--- a/qa/qa/page/project/import/github.rb
+++ b/qa/qa/page/project/import/github.rb
@@ -6,16 +6,16 @@ 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'
- element :list_repos_button, "submit_tag _('List your GitHub repositories')"
+ element :personal_access_token_field, 'text_field_tag :personal_access_token' # rubocop:disable QA/ElementWithPattern
+ element :list_repos_button, "submit_tag _('List your GitHub repositories')" # rubocop:disable QA/ElementWithPattern
end
view 'app/views/import/_githubish_status.html.haml' do
- element :project_import_row, 'data: { qa: { repo_path: repo.full_name } }'
+ element :project_import_row, 'data: { qa: { repo_path: repo.full_name } }' # rubocop:disable QA/ElementWithPattern
element :project_namespace_select
- element :project_namespace_field, 'select_tag :namespace_id'
- element :project_path_field, 'text_field_tag :path, sanitize_project_name(repo.name)'
- element :import_button, "_('Import')"
+ element :project_namespace_field, 'select_tag :namespace_id' # rubocop:disable QA/ElementWithPattern
+ element :project_path_field, 'text_field_tag :path, sanitize_project_name(repo.name)' # rubocop:disable QA/ElementWithPattern
+ element :import_button, "_('Import')" # rubocop:disable QA/ElementWithPattern
end
def add_personal_access_token(personal_access_token)
diff --git a/qa/qa/page/project/issue/index.rb b/qa/qa/page/project/issue/index.rb
index b5903f536a4..1035bf74a43 100644
--- a/qa/qa/page/project/issue/index.rb
+++ b/qa/qa/page/project/issue/index.rb
@@ -4,7 +4,7 @@ module QA
module Issue
class Index < Page::Base
view 'app/views/projects/issues/_issue.html.haml' do
- element :issue_link, 'link_to issue.title'
+ element :issue_link, 'link_to issue.title' # rubocop:disable QA/ElementWithPattern
end
def go_to_issue(title)
diff --git a/qa/qa/page/project/issue/new.rb b/qa/qa/page/project/issue/new.rb
index 7fc581da1ed..03b605ab24b 100644
--- a/qa/qa/page/project/issue/new.rb
+++ b/qa/qa/page/project/issue/new.rb
@@ -4,15 +4,15 @@ module QA
module Issue
class New < Page::Base
view 'app/views/shared/issuable/_form.html.haml' do
- element :submit_issue_button, 'form.submit "Submit'
+ element :submit_issue_button, 'form.submit "Submit' # rubocop:disable QA/ElementWithPattern
end
view 'app/views/shared/issuable/form/_title.html.haml' do
- element :issue_title_textbox, 'form.text_field :title'
+ element :issue_title_textbox, 'form.text_field :title' # rubocop:disable QA/ElementWithPattern
end
view 'app/views/shared/form_elements/_description.html.haml' do
- element :issue_description_textarea, "render 'projects/zen', f: form, attr: :description"
+ element :issue_description_textarea, "render 'projects/zen', f: form, attr: :description" # rubocop:disable QA/ElementWithPattern
end
def add_title(title)
diff --git a/qa/qa/page/project/issue/show.rb b/qa/qa/page/project/issue/show.rb
index 587a02163b9..23def93c7dd 100644
--- a/qa/qa/page/project/issue/show.rb
+++ b/qa/qa/page/project/issue/show.rb
@@ -5,35 +5,49 @@ module QA
module Project
module Issue
class Show < Page::Base
- view 'app/views/projects/issues/show.html.haml' do
- element :issue_details, '.issue-details'
- element :title, '.title'
- end
+ include Page::Component::Issuable::Common
view 'app/views/shared/notes/_form.html.haml' do
- element :new_note_form, 'new-note'
- element :new_note_form, 'attr: :note'
+ element :new_note_form, 'new-note' # rubocop:disable QA/ElementWithPattern
+ element :new_note_form, 'attr: :note' # rubocop:disable QA/ElementWithPattern
end
- view 'app/views/shared/notes/_comment_button.html.haml' do
- element :comment_button, '%strong Comment'
+ view 'app/assets/javascripts/notes/components/comment_form.vue' do
+ element :comment_button
+ element :comment_input
end
- def issue_title
- find('.issue-details .title').text
+ view 'app/assets/javascripts/notes/components/discussion_filter.vue' do
+ element :discussion_filter
+ element :filter_options
end
# Adds a comment to an issue
# attachment option should be an absolute path
def comment(text, attachment: nil)
- fill_in(with: text, name: 'note[note]')
+ fill_element :comment_input, text
unless attachment.nil?
QA::Page::Component::Dropzone.new(self, '.new-note')
.attach_file(attachment)
end
- click_on 'Comment'
+ click_element :comment_button
+ end
+
+ def select_comments_only_filter
+ click_element :discussion_filter
+ all_elements(:filter_options)[1].click
+ end
+
+ def select_history_only_filter
+ click_element :discussion_filter
+ all_elements(:filter_options).last.click
+ end
+
+ def select_all_activities_filter
+ click_element :discussion_filter
+ all_elements(:filter_options).first.click
end
end
end
diff --git a/qa/qa/page/project/job/show.rb b/qa/qa/page/project/job/show.rb
index 228ffd9d381..d688f15914c 100644
--- a/qa/qa/page/project/job/show.rb
+++ b/qa/qa/page/project/job/show.rb
@@ -4,30 +4,39 @@ module QA::Page
COMPLETED_STATUSES = %w[passed failed canceled blocked skipped manual].freeze # excludes created, pending, running
PASSED_STATUS = 'passed'.freeze
- view 'app/views/shared/builds/_build_output.html.haml' do
- element :build_output, '.js-build-output'
- element :loading_animation, '.js-build-refresh'
+ view 'app/assets/javascripts/jobs/components/job_app.vue' do
+ element :loading_animation
+ end
+
+ view 'app/assets/javascripts/jobs/components/job_log.vue' do
+ element :build_trace
end
view 'app/assets/javascripts/vue_shared/components/ci_badge_link.vue' do
- element :status_badge, 'ci-status'
+ element :status_badge
end
def completed?
- COMPLETED_STATUSES.include? find('.ci-status').text
+ COMPLETED_STATUSES.include?(status_badge)
end
def passed?
- find('.ci-status').text == PASSED_STATUS
+ status_badge == PASSED_STATUS
end
def trace_loading?
- has_css?('.js-build-refresh')
+ has_element?(:loading_animation)
end
# Reminder: You may wish to wait for a particular job status before checking output
def output
- find('.js-build-output').text
+ find_element(:build_trace).text
+ end
+
+ private
+
+ def status_badge
+ find_element(:status_badge).text
end
end
end
diff --git a/qa/qa/page/menu/side.rb b/qa/qa/page/project/menu.rb
index a1eedfea42e..cb4a10e1b6a 100644
--- a/qa/qa/page/menu/side.rb
+++ b/qa/qa/page/project/menu.rb
@@ -1,27 +1,32 @@
+# frozen_string_literal: true
+
module QA
module Page
- module Menu
- class Side < Page::Base
+ module Project
+ class Menu < Page::Base
view 'app/views/layouts/nav/sidebar/_project.html.haml' do
element :settings_item
- element :settings_link, 'link_to edit_project_path'
- element :repository_link, "title: _('Repository')"
+ element :settings_link, 'link_to edit_project_path' # rubocop:disable QA/ElementWithPattern
+ element :repository_link, "title: _('Repository')" # rubocop:disable QA/ElementWithPattern
element :link_pipelines
- element :pipelines_settings_link, "title: _('CI / CD')"
- element :operations_kubernetes_link, "title: _('Kubernetes')"
- element :issues_link, /link_to.*shortcuts-issues/
- element :issues_link_text, "Issues"
- element :merge_requests_link, /link_to.*shortcuts-merge_requests/
- element :merge_requests_link_text, "Merge Requests"
- element :top_level_items, '.sidebar-top-level-items'
- element :operations_section, "class: 'shortcuts-operations'"
- element :activity_link, "title: _('Activity')"
- element :wiki_link_text, "Wiki"
+ element :link_members_settings
+ element :pipelines_settings_link, "title: _('CI / CD')" # rubocop:disable QA/ElementWithPattern
+ element :operations_kubernetes_link, "title: _('Kubernetes')" # rubocop:disable QA/ElementWithPattern
+ element :operations_environments_link
+ element :issues_link, /link_to.*shortcuts-issues/ # rubocop:disable QA/ElementWithPattern
+ element :issues_link_text, "Issues" # rubocop:disable QA/ElementWithPattern
+ element :merge_requests_link, /link_to.*shortcuts-merge_requests/ # rubocop:disable QA/ElementWithPattern
+ element :merge_requests_link_text, "Merge Requests" # rubocop:disable QA/ElementWithPattern
+ element :top_level_items, '.sidebar-top-level-items' # rubocop:disable QA/ElementWithPattern
+ element :operations_section, "class: 'shortcuts-operations'" # rubocop:disable QA/ElementWithPattern
+ element :activity_link, "title: _('Activity')" # rubocop:disable QA/ElementWithPattern
+ element :wiki_link_text, "Wiki" # rubocop:disable QA/ElementWithPattern
element :milestones_link
+ element :labels_link
end
view 'app/assets/javascripts/fly_out_nav.js' do
- element :fly_out, "classList.add('fly-out-list')"
+ element :fly_out, "classList.add('fly-out-list')" # rubocop:disable QA/ElementWithPattern
end
def click_repository_settings
@@ -40,6 +45,22 @@ module QA
end
end
+ def click_operations_environments
+ hover_operations do
+ within_submenu do
+ click_element(:operations_environments_link)
+ end
+ end
+ end
+
+ def click_members_settings
+ hover_settings do
+ within_submenu do
+ click_element :link_members_settings
+ end
+ end
+ end
+
def click_operations_kubernetes
hover_operations do
within_submenu do
@@ -66,6 +87,14 @@ module QA
end
end
+ def go_to_labels
+ hover_issues do
+ within_submenu do
+ click_element(:labels_link)
+ end
+ end
+ end
+
def click_merge_requests
within_sidebar do
click_link('Merge Requests')
@@ -84,8 +113,22 @@ module QA
end
end
+ def click_repository
+ within_sidebar do
+ click_link('Repository')
+ end
+ end
+
private
+ def hover_issues
+ within_sidebar do
+ find_element(:issues_item).hover
+
+ yield
+ end
+ end
+
def hover_settings
within_sidebar do
find('.qa-settings-item').hover
diff --git a/qa/qa/page/project/new.rb b/qa/qa/page/project/new.rb
index 0766c98da6f..6acc413b586 100644
--- a/qa/qa/page/project/new.rb
+++ b/qa/qa/page/project/new.rb
@@ -5,21 +5,21 @@ module QA
include Page::Component::Select2
view 'app/views/projects/new.html.haml' do
- element :import_project_tab, "Import project"
+ element :import_project_tab, "Import project" # rubocop:disable QA/ElementWithPattern
end
view 'app/views/projects/_new_project_fields.html.haml' do
element :project_namespace_select
- element :project_namespace_field, 'namespaces_options'
- element :project_name, 'text_field :name'
- element :project_path, 'text_field :path'
- element :project_description, 'text_area :description'
- element :project_create_button, "submit 'Create project'"
- element :visibility_radios, 'visibility_level:'
+ element :project_namespace_field, 'namespaces_options' # rubocop:disable QA/ElementWithPattern
+ element :project_name, 'text_field :name' # rubocop:disable QA/ElementWithPattern
+ element :project_path, 'text_field :path' # rubocop:disable QA/ElementWithPattern
+ element :project_description, 'text_area :description' # rubocop:disable QA/ElementWithPattern
+ element :project_create_button, "submit 'Create project'" # rubocop:disable QA/ElementWithPattern
+ element :visibility_radios, 'visibility_level:' # rubocop:disable QA/ElementWithPattern
end
view 'app/views/projects/_import_project_pane.html.haml' do
- element :import_github, "icon('github', text: 'GitHub')"
+ element :import_github, "icon('github', text: 'GitHub')" # rubocop:disable QA/ElementWithPattern
end
def choose_test_namespace
diff --git a/qa/qa/page/project/operations/environments/index.rb b/qa/qa/page/project/operations/environments/index.rb
new file mode 100644
index 00000000000..63965a57edd
--- /dev/null
+++ b/qa/qa/page/project/operations/environments/index.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module QA
+ module Page
+ module Project
+ module Operations
+ module Environments
+ class Index < Page::Base
+ view 'app/assets/javascripts/environments/components/environment_item.vue' do
+ element :environment_link
+ end
+
+ def go_to_environment(environment_name)
+ wait(reload: false) do
+ find(element_selector_css(:environment_link), text: environment_name).click
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/project/operations/environments/show.rb b/qa/qa/page/project/operations/environments/show.rb
new file mode 100644
index 00000000000..aa88c218c89
--- /dev/null
+++ b/qa/qa/page/project/operations/environments/show.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module QA
+ module Page
+ module Project
+ module Operations
+ module Environments
+ class Show < Page::Base
+ view 'app/views/projects/environments/_external_url.html.haml' do
+ element :view_deployment
+ end
+
+ def view_deployment(&block)
+ new_window = window_opened_by { click_element(:view_deployment) }
+
+ within_window(new_window, &block) if block
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/project/operations/kubernetes/add.rb b/qa/qa/page/project/operations/kubernetes/add.rb
index 11ebe10fb18..939f912ea85 100644
--- a/qa/qa/page/project/operations/kubernetes/add.rb
+++ b/qa/qa/page/project/operations/kubernetes/add.rb
@@ -4,8 +4,8 @@ module QA
module Operations
module Kubernetes
class Add < Page::Base
- view 'app/views/projects/clusters/new.html.haml' do
- element :add_existing_cluster_button, "Add existing cluster"
+ view 'app/views/clusters/clusters/new.html.haml' do
+ element :add_existing_cluster_button, "Add existing cluster" # rubocop:disable QA/ElementWithPattern
end
def add_existing_cluster
diff --git a/qa/qa/page/project/operations/kubernetes/add_existing.rb b/qa/qa/page/project/operations/kubernetes/add_existing.rb
index eef82b5f329..f3ab636ecc1 100644
--- a/qa/qa/page/project/operations/kubernetes/add_existing.rb
+++ b/qa/qa/page/project/operations/kubernetes/add_existing.rb
@@ -4,12 +4,13 @@ module QA
module Operations
module Kubernetes
class AddExisting < Page::Base
- view 'app/views/projects/clusters/user/_form.html.haml' do
- element :cluster_name, 'text_field :name'
- element :api_url, 'text_field :api_url'
- element :ca_certificate, 'text_area :ca_cert'
- element :token, 'text_field :token'
- element :add_cluster_button, "submit s_('ClusterIntegration|Add Kubernetes cluster')"
+ view 'app/views/clusters/clusters/user/_form.html.haml' do
+ element :cluster_name, 'text_field :name' # rubocop:disable QA/ElementWithPattern
+ element :api_url, 'text_field :api_url' # rubocop:disable QA/ElementWithPattern
+ element :ca_certificate, 'text_area :ca_cert' # rubocop:disable QA/ElementWithPattern
+ element :token, 'text_field :token' # rubocop:disable QA/ElementWithPattern
+ element :add_cluster_button, "submit s_('ClusterIntegration|Add Kubernetes cluster')" # rubocop:disable QA/ElementWithPattern
+ element :rbac_checkbox
end
def set_cluster_name(name)
@@ -31,6 +32,10 @@ module QA
def add_cluster!
click_on 'Add Kubernetes cluster'
end
+
+ def check_rbac!
+ check_element :rbac_checkbox
+ end
end
end
end
diff --git a/qa/qa/page/project/operations/kubernetes/index.rb b/qa/qa/page/project/operations/kubernetes/index.rb
index 7261b5645da..67a74af1cd2 100644
--- a/qa/qa/page/project/operations/kubernetes/index.rb
+++ b/qa/qa/page/project/operations/kubernetes/index.rb
@@ -4,8 +4,8 @@ module QA
module Operations
module Kubernetes
class Index < Page::Base
- view 'app/views/projects/clusters/_empty_state.html.haml' do
- element :add_kubernetes_cluster_button, "link_to s_('ClusterIntegration|Add Kubernetes cluster')"
+ view 'app/views/clusters/clusters/_empty_state.html.haml' do
+ element :add_kubernetes_cluster_button, "link_to s_('ClusterIntegration|Add Kubernetes cluster')" # rubocop:disable QA/ElementWithPattern
end
def add_kubernetes_cluster
diff --git a/qa/qa/page/project/operations/kubernetes/show.rb b/qa/qa/page/project/operations/kubernetes/show.rb
index e831edeb89e..9e8f9ba79d7 100644
--- a/qa/qa/page/project/operations/kubernetes/show.rb
+++ b/qa/qa/page/project/operations/kubernetes/show.rb
@@ -5,13 +5,13 @@ module QA
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}'
- element :install_button, "s__('ClusterIntegration|Install')"
- element :installed_button, "s__('ClusterIntegration|Installed')"
+ element :application_row, 'js-cluster-application-row-${this.id}' # rubocop:disable QA/ElementWithPattern
+ element :install_button, "s__('ClusterIntegration|Install')" # rubocop:disable QA/ElementWithPattern
+ element :installed_button, "s__('ClusterIntegration|Installed')" # rubocop:disable QA/ElementWithPattern
end
view 'app/assets/javascripts/clusters/components/applications.vue' do
- element :ingress_ip_address, 'id="ingress-ip-address"'
+ element :ingress_ip_address, 'id="ingress-ip-address"' # rubocop:disable QA/ElementWithPattern
end
def install!(application_name)
diff --git a/qa/qa/page/project/pipeline/index.rb b/qa/qa/page/project/pipeline/index.rb
index ce430a2a6ee..19d83ecc4f4 100644
--- a/qa/qa/page/project/pipeline/index.rb
+++ b/qa/qa/page/project/pipeline/index.rb
@@ -2,7 +2,7 @@ module QA::Page
module Project::Pipeline
class Index < QA::Page::Base
view 'app/assets/javascripts/pipelines/components/pipeline_url.vue' do
- element :pipeline_link, 'class="js-pipeline-url-link"'
+ element :pipeline_link, 'class="js-pipeline-url-link"' # rubocop:disable QA/ElementWithPattern
end
def go_to_latest_pipeline
diff --git a/qa/qa/page/project/pipeline/show.rb b/qa/qa/page/project/pipeline/show.rb
index babc0079f3f..b22396fd67a 100644
--- a/qa/qa/page/project/pipeline/show.rb
+++ b/qa/qa/page/project/pipeline/show.rb
@@ -2,20 +2,20 @@ module QA::Page
module Project::Pipeline
class Show < QA::Page::Base
view 'app/assets/javascripts/vue_shared/components/header_ci_component.vue' do
- element :pipeline_header, /header class.*ci-header-container.*/
+ element :pipeline_header, /header class.*ci-header-container.*/ # rubocop:disable QA/ElementWithPattern
end
view 'app/assets/javascripts/pipelines/components/graph/graph_component.vue' do
- element :pipeline_graph, /class.*pipeline-graph.*/
+ element :pipeline_graph, /class.*pipeline-graph.*/ # rubocop:disable QA/ElementWithPattern
end
- view 'app/assets/javascripts/pipelines/components/graph/job_component.vue' do
- element :job_component, /class.*ci-job-component.*/
- element :job_link, /class.*js-pipeline-graph-job-link.*/
+ view 'app/assets/javascripts/pipelines/components/graph/job_item.vue' do
+ element :job_component, /class.*ci-job-component.*/ # rubocop:disable QA/ElementWithPattern
+ element :job_link, /class.*js-pipeline-graph-job-link.*/ # rubocop:disable QA/ElementWithPattern
end
view 'app/assets/javascripts/vue_shared/components/ci_icon.vue' do
- element :status_icon, 'ci-status-icon-${status}'
+ element :status_icon, 'ci-status-icon-${status}' # rubocop:disable QA/ElementWithPattern
end
def running?
diff --git a/qa/qa/page/project/settings/advanced.rb b/qa/qa/page/project/settings/advanced.rb
index d7b2b66b587..578f097e2dc 100644
--- a/qa/qa/page/project/settings/advanced.rb
+++ b/qa/qa/page/project/settings/advanced.rb
@@ -4,9 +4,9 @@ module QA
module Settings
class Advanced < Page::Base
view 'app/views/projects/edit.html.haml' do
- element :project_path_field, 'text_field :path'
- element :project_name_field, 'text_field :name'
- element :rename_project_button, "submit 'Rename project'"
+ element :project_path_field, 'text_field :path' # rubocop:disable QA/ElementWithPattern
+ element :project_name_field, 'text_field :name' # rubocop:disable QA/ElementWithPattern
+ element :rename_project_button, "submit 'Rename project'" # rubocop:disable QA/ElementWithPattern
end
def rename_to(path)
diff --git a/qa/qa/page/project/settings/ci_cd.rb b/qa/qa/page/project/settings/ci_cd.rb
index 752d3d93407..12c2409a5a7 100644
--- a/qa/qa/page/project/settings/ci_cd.rb
+++ b/qa/qa/page/project/settings/ci_cd.rb
@@ -12,11 +12,11 @@ module QA # rubocop:disable Naming/FileName
end
view 'app/views/projects/settings/ci_cd/_autodevops_form.html.haml' do
- element :enable_auto_devops_field, 'check_box :enabled'
- element :domain_field, 'text_field :domain'
- element :enable_auto_devops_button, "%strong= s_('CICD|Default to Auto DevOps pipeline')"
- element :domain_input, "%strong= _('Domain')"
- element :save_changes_button, "submit _('Save changes')"
+ element :enable_auto_devops_field, 'check_box :enabled' # rubocop:disable QA/ElementWithPattern
+ element :domain_field, 'text_field :domain' # rubocop:disable QA/ElementWithPattern
+ element :enable_auto_devops_button, "%strong= s_('CICD|Default to Auto DevOps pipeline')" # rubocop:disable QA/ElementWithPattern
+ element :domain_input, "%strong= _('Domain')" # rubocop:disable QA/ElementWithPattern
+ element :save_changes_button, "submit _('Save changes')" # rubocop:disable QA/ElementWithPattern
end
def expand_runners_settings(&block)
@@ -25,9 +25,9 @@ module QA # rubocop:disable Naming/FileName
end
end
- def expand_secret_variables(&block)
+ def expand_ci_variables(&block)
expand_section(:variables_settings) do
- Settings::SecretVariables.perform(&block)
+ Settings::CiVariables.perform(&block)
end
end
diff --git a/qa/qa/page/project/settings/secret_variables.rb b/qa/qa/page/project/settings/ci_variables.rb
index 937ae6797c8..e7a6e4bf628 100644
--- a/qa/qa/page/project/settings/secret_variables.rb
+++ b/qa/qa/page/project/settings/ci_variables.rb
@@ -2,18 +2,18 @@ module QA
module Page
module Project
module Settings
- class SecretVariables < Page::Base
+ class CiVariables < Page::Base
include Common
view 'app/views/ci/variables/_variable_row.html.haml' do
- element :variable_row, '.ci-variable-row-body'
- element :variable_key, '.qa-ci-variable-input-key'
- element :variable_value, '.qa-ci-variable-input-value'
+ element :variable_row, '.ci-variable-row-body' # rubocop:disable QA/ElementWithPattern
+ element :variable_key, '.qa-ci-variable-input-key' # rubocop:disable QA/ElementWithPattern
+ element :variable_value, '.qa-ci-variable-input-value' # rubocop:disable QA/ElementWithPattern
end
view 'app/views/ci/variables/_index.html.haml' do
- element :save_variables, '.js-secret-variables-save-button'
- element :reveal_values, '.js-secret-value-reveal-button'
+ element :save_variables, '.js-ci-variables-save-button' # rubocop:disable QA/ElementWithPattern
+ element :reveal_values, '.js-secret-value-reveal-button' # rubocop:disable QA/ElementWithPattern
end
def fill_variable(key, value)
@@ -33,7 +33,7 @@ module QA
end
def save_variables
- find('.js-secret-variables-save-button').click
+ find('.js-ci-variables-save-button').click
end
def reveal_variables
diff --git a/qa/qa/page/project/settings/common.rb b/qa/qa/page/project/settings/common.rb
index 874fb381554..f3b217677f2 100644
--- a/qa/qa/page/project/settings/common.rb
+++ b/qa/qa/page/project/settings/common.rb
@@ -8,7 +8,7 @@ module QA
def self.included(base)
base.class_eval do
view 'app/views/projects/edit.html.haml' do
- element :advanced_settings_expand, "= expanded ? 'Collapse' : 'Expand'"
+ element :advanced_settings_expand, "= expanded ? 'Collapse' : 'Expand'" # rubocop:disable QA/ElementWithPattern
end
end
end
diff --git a/qa/qa/page/project/settings/deploy_keys.rb b/qa/qa/page/project/settings/deploy_keys.rb
index 90a0e7092bd..3c8c0cbdf7c 100644
--- a/qa/qa/page/project/settings/deploy_keys.rb
+++ b/qa/qa/page/project/settings/deploy_keys.rb
@@ -4,18 +4,18 @@ module QA
module Settings
class DeployKeys < Page::Base
view 'app/views/projects/deploy_keys/_form.html.haml' do
- element :deploy_key_title, 'text_field :title'
- element :deploy_key_key, 'text_area :key'
+ element :deploy_key_title, 'text_field :title' # rubocop:disable QA/ElementWithPattern
+ element :deploy_key_key, 'text_area :key' # rubocop:disable QA/ElementWithPattern
end
view 'app/assets/javascripts/deploy_keys/components/app.vue' do
- element :deploy_keys_section, /class=".*deploy\-keys.*"/
- element :project_deploy_keys, 'class="qa-project-deploy-keys"'
+ element :deploy_keys_section, /class=".*deploy\-keys.*"/ # rubocop:disable QA/ElementWithPattern
+ element :project_deploy_keys, 'class="qa-project-deploy-keys"' # rubocop:disable QA/ElementWithPattern
end
view 'app/assets/javascripts/deploy_keys/components/key.vue' do
- element :key_title, /class=".*qa-key-title.*"/
- element :key_fingerprint, /class=".*qa-key-fingerprint.*"/
+ element :key_title, /class=".*qa-key-title.*"/ # rubocop:disable QA/ElementWithPattern
+ element :key_fingerprint, /class=".*qa-key-fingerprint.*"/ # rubocop:disable QA/ElementWithPattern
end
def fill_key_title(title)
diff --git a/qa/qa/page/project/settings/deploy_tokens.rb b/qa/qa/page/project/settings/deploy_tokens.rb
new file mode 100644
index 00000000000..2d42372cbc5
--- /dev/null
+++ b/qa/qa/page/project/settings/deploy_tokens.rb
@@ -0,0 +1,64 @@
+module QA
+ module Page
+ module Project
+ module Settings
+ class DeployTokens < Page::Base
+ view 'app/views/projects/deploy_tokens/_form.html.haml' do
+ element :deploy_token_name
+ element :deploy_token_expires_at
+ element :deploy_token_read_repository
+ element :deploy_token_read_registry
+ element :create_deploy_token
+ end
+
+ view 'app/views/projects/deploy_tokens/_new_deploy_token.html.haml' do
+ element :created_deploy_token_section
+ element :deploy_token_user
+ element :deploy_token
+ end
+
+ def fill_token_name(name)
+ fill_element :deploy_token_name, name
+ end
+
+ def fill_token_expires_at(expires_at)
+ fill_element :deploy_token_expires_at, expires_at.to_s + "\n"
+ end
+
+ def fill_scopes(read_repository:, read_registry:)
+ check_element :deploy_token_read_repository if read_repository
+ check_element :deploy_token_read_registry if read_registry
+ end
+
+ def add_token
+ click_element :create_deploy_token
+ end
+
+ def token_username
+ within_new_project_deploy_token do
+ find_element(:deploy_token_user).value
+ end
+ end
+
+ def token_password
+ within_new_project_deploy_token do
+ find_element(:deploy_token).value
+ end
+ end
+
+ private
+
+ def within_new_project_deploy_token
+ wait(reload: false) do
+ has_css?(element_selector_css(:created_deploy_token_section))
+ end
+
+ within_element(:created_deploy_token_section) do
+ yield
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/project/settings/members.rb b/qa/qa/page/project/settings/members.rb
new file mode 100644
index 00000000000..7fed93ca83f
--- /dev/null
+++ b/qa/qa/page/project/settings/members.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module QA
+ module Page
+ module Project
+ module Settings
+ class Members < Page::Base
+ include Page::Component::UsersSelect
+
+ view 'app/views/projects/project_members/_new_project_member.html.haml' do
+ element :member_select_input
+ element :add_member_button
+ end
+
+ view 'app/views/projects/project_members/_team.html.haml' do
+ element :members_list
+ end
+
+ def add_member(username)
+ select_user :member_select_input, username
+ click_element :add_member_button
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/project/settings/repository.rb b/qa/qa/page/project/settings/repository.rb
index 1ed5f455a85..53ebe28970b 100644
--- a/qa/qa/page/project/settings/repository.rb
+++ b/qa/qa/page/project/settings/repository.rb
@@ -24,6 +24,12 @@ module QA
ProtectedBranches.perform(&block)
end
end
+
+ def expand_deploy_tokens(&block)
+ expand_section(:deploy_tokens_settings) do
+ DeployTokens.perform(&block)
+ end
+ end
end
end
end
diff --git a/qa/qa/page/project/settings/runners.rb b/qa/qa/page/project/settings/runners.rb
index b41668c94cd..ac930f5385a 100644
--- a/qa/qa/page/project/settings/runners.rb
+++ b/qa/qa/page/project/settings/runners.rb
@@ -4,8 +4,8 @@ module QA
module Settings
class Runners < Page::Base
view 'app/views/ci/runner/_how_to_setup_runner.html.haml' do
- element :registration_token, '%code#registration_token'
- element :coordinator_address, '%code#coordinator_address'
+ element :registration_token, '%code#registration_token' # rubocop:disable QA/ElementWithPattern
+ element :coordinator_address, '%code#coordinator_address' # rubocop:disable QA/ElementWithPattern
end
##
@@ -13,7 +13,7 @@ module QA
#
view 'app/helpers/runners_helper.rb' do
# rubocop:disable Lint/InterpolationCheck
- element :runner_status, 'runner-status-#{status}'
+ element :runner_status, 'runner-status-#{status}' # rubocop:disable QA/ElementWithPattern
# rubocop:enable Lint/InterpolationCheck
end
diff --git a/qa/qa/page/project/show.rb b/qa/qa/page/project/show.rb
index 267e7bbc249..d6dddf03ffb 100644
--- a/qa/qa/page/project/show.rb
+++ b/qa/qa/page/project/show.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module QA
module Page
module Project
@@ -14,7 +16,7 @@ module QA
view 'app/views/layouts/header/_new_dropdown.haml' do
element :new_menu_toggle
- element :new_issue_link, "link_to _('New issue'), new_project_issue_path(@project)"
+ element :new_issue_link, "link_to _('New issue'), new_project_issue_path(@project)" # rubocop:disable QA/ElementWithPattern
end
view 'app/views/shared/_ref_switcher.html.haml' do
@@ -23,24 +25,40 @@ module QA
end
view 'app/views/projects/buttons/_fork.html.haml' do
- element :fork_label, "%span= s_('ProjectOverview|Fork')"
- element :fork_link, "link_to new_project_fork_path(@project)"
+ element :fork_label, "%span= s_('ProjectOverview|Fork')" # rubocop:disable QA/ElementWithPattern
+ element :fork_link, "link_to new_project_fork_path(@project)" # rubocop:disable QA/ElementWithPattern
end
view 'app/views/projects/_files.html.haml' do
- element :tree_holder, '.tree-holder'
+ element :tree_holder, '.tree-holder' # rubocop:disable QA/ElementWithPattern
+ end
+
+ view 'app/views/projects/buttons/_dropdown.html.haml' do
+ element :create_new_dropdown
+ element :new_file_option
+ end
+
+ view 'app/views/projects/tree/_tree_header.html.haml' do
+ element :web_ide_button
end
- view 'app/presenters/project_presenter.rb' do
- element :new_file_button, "_('New file'),"
+ view 'app/views/projects/tree/_tree_content.html.haml' do
+ element :file_tree
end
def project_name
find('.qa-project-name').text
end
- def go_to_new_file!
- click_on 'New file'
+ def create_new_file!
+ click_element :create_new_dropdown
+ click_element :new_file_option
+ end
+
+ def go_to_file(filename)
+ within_element(:file_tree) do
+ click_on filename
+ end
end
def switch_to_branch(branch_name)
@@ -78,6 +96,10 @@ module QA
def fork_project
click_on 'Fork'
end
+
+ def open_web_ide!
+ click_element :web_ide_button
+ end
end
end
end
diff --git a/qa/qa/page/project/web_ide/edit.rb b/qa/qa/page/project/web_ide/edit.rb
new file mode 100644
index 00000000000..23e580b81b6
--- /dev/null
+++ b/qa/qa/page/project/web_ide/edit.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+module QA
+ module Page
+ module Project
+ module WebIDE
+ class Edit < Page::Base
+ include Page::Component::DropdownFilter
+
+ view 'app/assets/javascripts/ide/components/ide_tree.vue' do
+ element :new_file
+ end
+
+ view 'app/assets/javascripts/ide/components/ide_tree_list.vue' do
+ element :file_list
+ end
+
+ view 'app/assets/javascripts/ide/components/new_dropdown/modal.vue' do
+ element :full_file_path
+ element :template_list
+ end
+
+ view 'app/assets/javascripts/ide/components/file_templates/bar.vue' do
+ element :file_templates_bar
+ element :file_template_dropdown
+ end
+
+ view 'app/assets/javascripts/ide/components/file_templates/dropdown.vue' do
+ element :dropdown_filter_input
+ end
+
+ view 'app/assets/javascripts/ide/components/commit_sidebar/form.vue' do
+ element :begin_commit_button
+ element :commit_button
+ end
+
+ def has_file?(file_name)
+ within_element(:file_list) do
+ page.has_content? file_name
+ end
+ end
+
+ def create_new_file_from_template(file_name, template)
+ click_element :new_file
+ within_element(:template_list) do
+ begin
+ click_on file_name
+ rescue Capybara::ElementNotFound
+ raise ElementNotFound, %Q(Couldn't find file template named "#{file_name}". Please confirm that it is a valid option.)
+ end
+ end
+
+ wait(reload: false) do
+ within_element(:file_templates_bar) do
+ click_element :file_template_dropdown
+ fill_element :dropdown_filter_input, template
+
+ begin
+ click_on template
+ rescue Capybara::ElementNotFound
+ raise ElementNotFound, %Q(Couldn't find template "#{template}" for #{file_name}. Please confirm that it exists in the list of templates.)
+ end
+ end
+ end
+ end
+
+ def commit_changes
+ click_element :begin_commit_button
+ click_element :commit_button
+
+ wait(reload: false) do
+ page.has_content?('Your changes have been committed')
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/project/wiki/edit.rb b/qa/qa/page/project/wiki/edit.rb
index 6fa45569cc0..8d0eafa1818 100644
--- a/qa/qa/page/project/wiki/edit.rb
+++ b/qa/qa/page/project/wiki/edit.rb
@@ -4,9 +4,9 @@ module QA
module Wiki
class Edit < Page::Base
view 'app/views/projects/wikis/_main_links.html.haml' do
- element :new_page_link, 'New page'
- element :page_history_link, 'Page history'
- element :edit_page_link, 'Edit'
+ element :new_page_link, 'New page' # rubocop:disable QA/ElementWithPattern
+ element :page_history_link, 'Page history' # rubocop:disable QA/ElementWithPattern
+ element :edit_page_link, 'Edit' # rubocop:disable QA/ElementWithPattern
end
def go_to_new_page
diff --git a/qa/qa/page/project/wiki/new.rb b/qa/qa/page/project/wiki/new.rb
index 415b3835538..2498af8600c 100644
--- a/qa/qa/page/project/wiki/new.rb
+++ b/qa/qa/page/project/wiki/new.rb
@@ -4,15 +4,15 @@ module QA
module Wiki
class New < Page::Base
view 'app/views/projects/wikis/_form.html.haml' do
- element :wiki_title_textbox, 'text_field :title'
- element :wiki_content_textarea, "render 'projects/zen', f: f, attr: :content"
- element :wiki_message_textbox, 'text_field :message'
- element :save_changes_button, 'submit _("Save changes")'
- element :create_page_button, 'submit s_("Wiki|Create page")'
+ element :wiki_title_textbox, 'text_field :title' # rubocop:disable QA/ElementWithPattern
+ element :wiki_content_textarea, "render 'projects/zen', f: f, attr: :content" # rubocop:disable QA/ElementWithPattern
+ element :wiki_message_textbox, 'text_field :message' # rubocop:disable QA/ElementWithPattern
+ element :save_changes_button, 'submit _("Save changes")' # rubocop:disable QA/ElementWithPattern
+ element :create_page_button, 'submit s_("Wiki|Create page")' # rubocop:disable QA/ElementWithPattern
end
view 'app/views/shared/empty_states/_wikis.html.haml' do
- element :create_link, 'Create your first page'
+ element :create_link, 'Create your first page' # rubocop:disable QA/ElementWithPattern
end
def go_to_create_first_page
diff --git a/qa/qa/page/project/wiki/show.rb b/qa/qa/page/project/wiki/show.rb
index c47a715687f..a7c4455d080 100644
--- a/qa/qa/page/project/wiki/show.rb
+++ b/qa/qa/page/project/wiki/show.rb
@@ -8,7 +8,7 @@ module QA
include Page::Component::ClonePanel
view 'app/views/projects/wikis/pages.html.haml' do
- element :clone_repository_link, 'Clone repository'
+ element :clone_repository_link, 'Clone repository' # rubocop:disable QA/ElementWithPattern
end
def go_to_clone_repository
diff --git a/qa/qa/resource/README.md b/qa/qa/resource/README.md
new file mode 100644
index 00000000000..4cdeb3f42a2
--- /dev/null
+++ b/qa/qa/resource/README.md
@@ -0,0 +1,392 @@
+# Resource class in GitLab QA
+
+Resources are primarily created using Browser UI steps, but can also
+be created via the API.
+
+## How to properly implement a resource class?
+
+All resource classes should inherit from [`Resource::Base`](./base.rb).
+
+There is only one mandatory method to implement to define a resource class.
+This is the `#fabricate!` method, which is used to build the resource via the
+browser UI. Note that you should only use [Page objects](../page/README.md) to
+interact with a Web page in this method.
+
+Here is an imaginary example:
+
+```ruby
+module QA
+ module Resource
+ class Shirt < Base
+ attr_accessor :name
+
+ def fabricate!
+ Page::Dashboard::Index.perform do |dashboard_index|
+ dashboard_index.go_to_new_shirt
+ end
+
+ Page::Shirt::New.perform do |shirt_new|
+ shirt_new.set_name(name)
+ shirt_new.create_shirt!
+ end
+ end
+ end
+ end
+end
+```
+
+### Define API implementation
+
+A resource class may also implement the three following methods to be able to
+create the resource via the public GitLab API:
+
+- `#api_get_path`: The `GET` path to fetch an existing resource.
+- `#api_post_path`: The `POST` path to create a new resource.
+- `#api_post_body`: The `POST` body (as a Ruby hash) to create a new resource.
+
+Let's take the `Shirt` resource class, and add these three API methods:
+
+```ruby
+module QA
+ module Resource
+ class Shirt < Base
+ attr_accessor :name
+
+ def fabricate!
+ # ... same as before
+ end
+
+ def api_get_path
+ "/shirt/#{name}"
+ end
+
+ def api_post_path
+ "/shirts"
+ end
+
+ def api_post_body
+ {
+ name: name
+ }
+ end
+ end
+ end
+end
+```
+
+The [`Project` resource](./project.rb) is a good real example of Browser
+UI and API implementations.
+
+#### Resource attributes
+
+A resource may need another resource to exist first. For instance, a project
+needs a group to be created in.
+
+To define a resource attribute, you can use the `attribute` method with a
+block using the other resource class to fabricate the resource.
+
+That will allow access to the other resource from your resource object's
+methods. You would usually use it in `#fabricate!`, `#api_get_path`,
+`#api_post_path`, `#api_post_body`.
+
+Let's take the `Shirt` resource class, and add a `project` attribute to it:
+
+```ruby
+module QA
+ module Resource
+ class Shirt < Base
+ attr_accessor :name
+
+ attribute :project do
+ Project.fabricate! do |resource|
+ resource.name = 'project-to-create-a-shirt'
+ end
+ end
+
+ def fabricate!
+ project.visit!
+
+ Page::Project::Show.perform do |project_show|
+ project_show.go_to_new_shirt
+ end
+
+ Page::Shirt::New.perform do |shirt_new|
+ shirt_new.set_name(name)
+ shirt_new.create_shirt!
+ end
+ end
+
+ def api_get_path
+ "/project/#{project.path}/shirt/#{name}"
+ end
+
+ def api_post_path
+ "/project/#{project.path}/shirts"
+ end
+
+ def api_post_body
+ {
+ name: name
+ }
+ end
+ end
+ end
+end
+```
+
+**Note that all the attributes are lazily constructed. This means if you want
+a specific attribute to be fabricated first, you'll need to call the
+attribute method first even if you're not using it.**
+
+#### Product data attributes
+
+Once created, you may want to populate a resource with attributes that can be
+found in the Web page, or in the API response.
+For instance, once you create a project, you may want to store its repository
+SSH URL as an attribute.
+
+Again we could use the `attribute` method with a block, using a page object
+to retrieve the data on the page.
+
+Let's take the `Shirt` resource class, and define a `:brand` attribute:
+
+```ruby
+module QA
+ module Resource
+ class Shirt < Base
+ attr_accessor :name
+
+ attribute :project do
+ Project.fabricate! do |resource|
+ resource.name = 'project-to-create-a-shirt'
+ end
+ end
+
+ # Attribute populated from the Browser UI (using the block)
+ attribute :brand do
+ Page::Shirt::Show.perform do |shirt_show|
+ shirt_show.fetch_brand_from_page
+ end
+ end
+
+ # ... same as before
+ end
+ end
+end
+```
+
+**Note again that all the attributes are lazily constructed. This means if
+you call `shirt.brand` after moving to the other page, it'll not properly
+retrieve the data because we're no longer on the expected page.**
+
+Consider this:
+
+```ruby
+shirt =
+ QA::Resource::Shirt.fabricate! do |resource|
+ resource.name = "GitLab QA"
+ end
+
+shirt.project.visit!
+
+shirt.brand # => FAIL!
+```
+
+The above example will fail because now we're on the project page, trying to
+construct the brand data from the shirt page, however we moved to the project
+page already. There are two ways to solve this, one is that we could try to
+retrieve the brand before visiting the project again:
+
+```ruby
+shirt =
+ QA::Resource::Shirt.fabricate! do |resource|
+ resource.name = "GitLab QA"
+ end
+
+shirt.brand # => OK!
+
+shirt.project.visit!
+
+shirt.brand # => OK!
+```
+
+The attribute will be stored in the instance therefore all the following calls
+will be fine, using the data previously constructed. If we think that this
+might be too brittle, we could eagerly construct the data right before
+ending fabrication:
+
+```ruby
+module QA
+ module Resource
+ class Shirt < Base
+ # ... same as before
+
+ def fabricate!
+ project.visit!
+
+ Page::Project::Show.perform do |project_show|
+ project_show.go_to_new_shirt
+ end
+
+ Page::Shirt::New.perform do |shirt_new|
+ shirt_new.set_name(name)
+ shirt_new.create_shirt!
+ end
+
+ populate(:brand) # Eagerly construct the data
+ end
+ end
+ end
+end
+```
+
+The `populate` method will iterate through its arguments and call each
+attribute respectively. Here `populate(:brand)` has the same effect as
+just `brand`. Using the populate method makes the intention clearer.
+
+With this, it will make sure we construct the data right after we create the
+shirt. The drawback is that this will always construct the data when the
+resource is fabricated even if we don't need to use the data.
+
+Alternatively, we could just make sure we're on the right page before
+constructing the brand data:
+
+```ruby
+module QA
+ module Resource
+ class Shirt < Base
+ attr_accessor :name
+
+ attribute :project do
+ Project.fabricate! do |resource|
+ resource.name = 'project-to-create-a-shirt'
+ end
+ end
+
+ # Attribute populated from the Browser UI (using the block)
+ attribute :brand do
+ back_url = current_url
+ visit!
+
+ Page::Shirt::Show.perform do |shirt_show|
+ shirt_show.fetch_brand_from_page
+ end
+
+ visit(back_url)
+ end
+
+ # ... same as before
+ end
+ end
+end
+```
+
+This will make sure it's on the shirt page before constructing brand, and
+move back to the previous page to avoid breaking the state.
+
+#### Define an attribute based on an API response
+
+Sometimes, you want to define a resource attribute based on the API response
+from its `GET` or `POST` request. For instance, if the creation of a shirt via
+the API returns
+
+```ruby
+{
+ brand: 'a-brand-new-brand',
+ style: 't-shirt',
+ materials: [[:cotton, 80], [:polyamide, 20]]
+}
+```
+
+you may want to store `style` as-is in the resource, and fetch the first value
+of the first `materials` item in a `main_fabric` attribute.
+
+Let's take the `Shirt` resource class, and define a `:style` and a
+`:main_fabric` attributes:
+
+```ruby
+module QA
+ module Resource
+ class Shirt < Base
+ # ... same as before
+
+ # @style from the instance if present,
+ # or fetched from the API response if present,
+ # or a QA::Resource::Base::NoValueError is raised otherwise
+ attribute :style
+
+ # If @main_fabric is not present,
+ # and if the API does not contain this field, this block will be
+ # used to construct the value based on the API response, and
+ # store the result in @main_fabric
+ attribute :main_fabric do
+ api_response.&dig(:materials, 0, 0)
+ end
+
+ # ... same as before
+ end
+ end
+end
+```
+
+**Notes on attributes precedence:**
+
+- resource instance variables have the highest precedence
+- attributes from the API response take precedence over attributes from the
+ block (usually from Browser UI)
+- attributes without a value will raise a `QA::Resource::Base::NoValueError` error
+
+## Creating resources in your tests
+
+To create a resource in your tests, you can call the `.fabricate!` method on
+the resource class.
+Note that if the resource class supports API fabrication, this will use this
+fabrication by default.
+
+Here is an example that will use the API fabrication method under the hood
+since it's supported by the `Shirt` resource class:
+
+```ruby
+my_shirt = Resource::Shirt.fabricate! do |shirt|
+ shirt.name = 'my-shirt'
+end
+
+expect(page).to have_text(my_shirt.name) # => "my-shirt" from the resource's instance variable
+expect(page).to have_text(my_shirt.brand) # => "a-brand-new-brand" from the API response
+expect(page).to have_text(my_shirt.style) # => "t-shirt" from the API response
+expect(page).to have_text(my_shirt.main_fabric) # => "cotton" from the API response via the block
+```
+
+If you explicitly want to use the Browser UI fabrication method, you can call
+the `.fabricate_via_browser_ui!` method instead:
+
+```ruby
+my_shirt = Resource::Shirt.fabricate_via_browser_ui! do |shirt|
+ shirt.name = 'my-shirt'
+end
+
+expect(page).to have_text(my_shirt.name) # => "my-shirt" from the resource's instance variable
+expect(page).to have_text(my_shirt.brand) # => the brand name fetched from the `Page::Shirt::Show` page via the block
+expect(page).to have_text(my_shirt.style) # => QA::Resource::Base::NoValueError will be raised because no API response nor a block is provided
+expect(page).to have_text(my_shirt.main_fabric) # => QA::Resource::Base::NoValueError will be raised because no API response and the block didn't provide a value (because it's also based on the API response)
+```
+
+You can also explicitly use the API fabrication method, by calling the
+`.fabricate_via_api!` method:
+
+```ruby
+my_shirt = Resource::Shirt.fabricate_via_api! do |shirt|
+ shirt.name = 'my-shirt'
+end
+```
+
+In this case, the result will be similar to calling
+`Resource::Shirt.fabricate!`.
+
+## Where to ask for help?
+
+If you need more information, ask for help on `#quality` channel on Slack
+(internal, GitLab Team only).
+
+If you are not a Team Member, and you still need help to contribute, please
+open an issue in GitLab CE issue tracker with the `~QA` label.
diff --git a/qa/qa/resource/api_fabricator.rb b/qa/qa/resource/api_fabricator.rb
new file mode 100644
index 00000000000..98eebac0880
--- /dev/null
+++ b/qa/qa/resource/api_fabricator.rb
@@ -0,0 +1,96 @@
+# frozen_string_literal: true
+
+require 'active_support/core_ext/object/deep_dup'
+require 'capybara/dsl'
+
+module QA
+ module Resource
+ module ApiFabricator
+ include Capybara::DSL
+
+ HTTP_STATUS_OK = 200
+ HTTP_STATUS_CREATED = 201
+
+ ResourceNotFoundError = Class.new(RuntimeError)
+ ResourceFabricationFailedError = Class.new(RuntimeError)
+ ResourceURLMissingError = Class.new(RuntimeError)
+
+ attr_reader :api_resource, :api_response
+
+ def api_support?
+ respond_to?(:api_get_path) &&
+ respond_to?(:api_post_path) &&
+ respond_to?(:api_post_body)
+ end
+
+ def fabricate_via_api!
+ unless api_support?
+ raise NotImplementedError, "Resource #{self.class.name} does not support fabrication via the API!"
+ end
+
+ resource_web_url(api_post)
+ end
+
+ def eager_load_api_client!
+ api_client.tap do |client|
+ # Eager-load the API client so that the personal token creation isn't
+ # taken in account in the actual resource creation timing.
+ client.personal_access_token
+ end
+ end
+
+ private
+
+ include Support::Api
+ attr_writer :api_resource, :api_response
+
+ def resource_web_url(resource)
+ resource.fetch(:web_url) do
+ raise ResourceURLMissingError, "API resource for #{self.class.name} does not expose a `web_url` property: `#{resource}`."
+ end
+ end
+
+ def api_get
+ process_api_response(parse_body(api_get_from(api_get_path)))
+ end
+
+ def api_get_from(get_path)
+ url = Runtime::API::Request.new(api_client, get_path).url
+ response = get(url)
+
+ unless response.code == HTTP_STATUS_OK
+ raise ResourceNotFoundError, "Resource at #{url} could not be found (#{response.code}): `#{response}`."
+ end
+
+ response
+ end
+
+ def api_post
+ response = post(
+ Runtime::API::Request.new(api_client, api_post_path).url,
+ api_post_body)
+
+ unless response.code == HTTP_STATUS_CREATED
+ raise ResourceFabricationFailedError, "Fabrication of #{self.class.name} using the API failed (#{response.code}) with `#{response}`."
+ end
+
+ process_api_response(parse_body(response))
+ end
+
+ def api_client
+ @api_client ||= begin
+ Runtime::API::Client.new(:gitlab, is_new_session: !current_url.start_with?('http'))
+ end
+ end
+
+ def process_api_response(parsed_response)
+ self.api_response = parsed_response
+ self.api_resource = transform_api_resource(parsed_response.deep_dup)
+ end
+
+ def transform_api_resource(api_resource)
+ api_resource
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/base.rb b/qa/qa/resource/base.rb
new file mode 100644
index 00000000000..dcea144ab74
--- /dev/null
+++ b/qa/qa/resource/base.rb
@@ -0,0 +1,155 @@
+# frozen_string_literal: true
+
+require 'forwardable'
+require 'capybara/dsl'
+require 'active_support/core_ext/array/extract_options'
+
+module QA
+ module Resource
+ class Base
+ extend SingleForwardable
+ include ApiFabricator
+ extend Capybara::DSL
+
+ NoValueError = Class.new(RuntimeError)
+
+ def_delegators :evaluator, :attribute
+
+ def fabricate!(*_args)
+ raise NotImplementedError
+ end
+
+ def visit!
+ visit(web_url)
+ end
+
+ def populate(*attributes)
+ attributes.each(&method(:public_send))
+ end
+
+ private
+
+ def populate_attribute(name, block)
+ value = attribute_value(name, block)
+
+ raise NoValueError, "No value was computed for #{name} of #{self.class.name}." unless value
+
+ value
+ end
+
+ def attribute_value(name, block)
+ api_value = api_resource&.dig(name)
+
+ if api_value && block
+ log_having_both_api_result_and_block(name, api_value)
+ end
+
+ api_value || (block && instance_exec(&block))
+ end
+
+ def log_having_both_api_result_and_block(name, api_value)
+ QA::Runtime::Logger.info "<#{self.class}> Attribute #{name.inspect} has both API response `#{api_value}` and a block. API response will be picked. Block will be ignored."
+ end
+
+ def self.fabricate!(*args, &prepare_block)
+ fabricate_via_api!(*args, &prepare_block)
+ rescue NotImplementedError
+ fabricate_via_browser_ui!(*args, &prepare_block)
+ end
+
+ def self.fabricate_via_browser_ui!(*args, &prepare_block)
+ options = args.extract_options!
+ resource = options.fetch(:resource) { new }
+ parents = options.fetch(:parents) { [] }
+
+ do_fabricate!(resource: resource, prepare_block: prepare_block, parents: parents) do
+ log_fabrication(:browser_ui, resource, parents, args) { resource.fabricate!(*args) }
+
+ current_url
+ end
+ end
+
+ def self.fabricate_via_api!(*args, &prepare_block)
+ options = args.extract_options!
+ resource = options.fetch(:resource) { new }
+ parents = options.fetch(:parents) { [] }
+
+ raise NotImplementedError unless resource.api_support?
+
+ resource.eager_load_api_client!
+
+ do_fabricate!(resource: resource, prepare_block: prepare_block, parents: parents) do
+ log_fabrication(:api, resource, parents, args) { resource.fabricate_via_api! }
+ end
+ end
+
+ def self.do_fabricate!(resource:, prepare_block:, parents: [])
+ prepare_block.call(resource) if prepare_block
+
+ resource_web_url = yield
+ resource.web_url = resource_web_url
+
+ resource
+ end
+ private_class_method :do_fabricate!
+
+ def self.log_fabrication(method, resource, parents, args)
+ return yield unless Runtime::Env.debug?
+
+ start = Time.now
+ prefix = "==#{'=' * parents.size}>"
+ msg = [prefix]
+ msg << "Built a #{name}"
+ msg << "as a dependency of #{parents.last}" if parents.any?
+ msg << "via #{method}"
+
+ yield.tap do
+ msg << "in #{Time.now - start} seconds"
+ puts msg.join(' ')
+ puts if parents.empty?
+ end
+ end
+ private_class_method :log_fabrication
+
+ def self.evaluator
+ @evaluator ||= Base::DSL.new(self)
+ end
+ private_class_method :evaluator
+
+ def self.dynamic_attributes
+ const_get(:DynamicAttributes)
+ rescue NameError
+ mod = const_set(:DynamicAttributes, Module.new)
+
+ include mod
+
+ mod
+ end
+
+ def self.attributes_names
+ dynamic_attributes.instance_methods(false).sort.grep_v(/=$/)
+ end
+
+ class DSL
+ def initialize(base)
+ @base = base
+ end
+
+ def attribute(name, &block)
+ @base.dynamic_attributes.module_eval do
+ attr_writer(name)
+
+ define_method(name) do
+ instance_variable_get("@#{name}") ||
+ instance_variable_set(
+ "@#{name}",
+ populate_attribute(name, block))
+ end
+ end
+ end
+ end
+
+ attribute :web_url
+ end
+ end
+end
diff --git a/qa/qa/resource/branch.rb b/qa/qa/resource/branch.rb
new file mode 100644
index 00000000000..bd52c4abe02
--- /dev/null
+++ b/qa/qa/resource/branch.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+module QA
+ module Resource
+ class Branch < Base
+ attr_accessor :project, :branch_name,
+ :allow_to_push, :allow_to_merge, :protected
+
+ attribute :project do
+ Project.fabricate! do |resource|
+ resource.name = 'protected-branch-project'
+ end
+ end
+
+ def initialize
+ @branch_name = 'test/branch'
+ @allow_to_push = true
+ @allow_to_merge = true
+ @protected = false
+ end
+
+ def fabricate!
+ project.visit!
+
+ Repository::ProjectPush.fabricate! do |resource|
+ resource.project = project
+ resource.file_name = 'kick-off.txt'
+ resource.commit_message = 'First commit'
+ end
+
+ branch = Repository::ProjectPush.fabricate! do |resource|
+ resource.project = project
+ resource.file_name = 'README.md'
+ resource.commit_message = 'Add readme'
+ resource.branch_name = 'master'
+ resource.new_branch = false
+ resource.remote_branch = @branch_name
+ end
+
+ Page::Project::Show.perform do |page|
+ page.wait { page.has_content?(branch_name) }
+ end
+
+ # The upcoming process will make it access the Protected Branches page,
+ # select the already created branch and protect it according
+ # to `allow_to_push` variable.
+ return branch unless @protected
+
+ Page::Project::Menu.perform(&:click_repository_settings)
+
+ Page::Project::Settings::Repository.perform do |setting|
+ setting.expand_protected_branches do |page|
+ page.select_branch(branch_name)
+
+ if allow_to_push
+ page.allow_devs_and_maintainers_to_push
+ else
+ page.allow_no_one_to_push
+ end
+
+ if allow_to_merge
+ page.allow_devs_and_maintainers_to_merge
+ else
+ page.allow_no_one_to_merge
+ end
+
+ page.wait(reload: false) do
+ !page.first('.btn-success').disabled?
+ end
+
+ page.protect_branch
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/ci_variable.rb b/qa/qa/resource/ci_variable.rb
new file mode 100644
index 00000000000..0570c47d41c
--- /dev/null
+++ b/qa/qa/resource/ci_variable.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module QA
+ module Resource
+ class CiVariable < Base
+ attr_accessor :key, :value
+
+ attribute :project do
+ Project.fabricate! do |resource|
+ resource.name = 'project-with-ci-variables'
+ resource.description = 'project for adding CI variable test'
+ end
+ end
+
+ def fabricate!
+ project.visit!
+
+ Page::Project::Menu.perform(&:click_ci_cd_settings)
+
+ Page::Project::Settings::CICD.perform do |setting|
+ setting.expand_ci_variables do |page|
+ page.fill_variable(key, value)
+
+ page.save_variables
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/deploy_key.rb b/qa/qa/resource/deploy_key.rb
new file mode 100644
index 00000000000..9ed8fb7726e
--- /dev/null
+++ b/qa/qa/resource/deploy_key.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module QA
+ module Resource
+ class DeployKey < Base
+ attr_accessor :title, :key
+
+ attribute :fingerprint do
+ Page::Project::Settings::Repository.perform do |setting|
+ setting.expand_deploy_keys do |key|
+ key_offset = key.key_titles.index do |key_title|
+ key_title.text == title
+ end
+
+ key.key_fingerprints[key_offset].text
+ end
+ end
+ end
+
+ attribute :project do
+ Project.fabricate! do |resource|
+ resource.name = 'project-to-deploy'
+ resource.description = 'project for adding deploy key test'
+ end
+ end
+
+ def fabricate!
+ project.visit!
+
+ Page::Project::Menu.perform(&:click_repository_settings)
+
+ Page::Project::Settings::Repository.perform do |setting|
+ setting.expand_deploy_keys do |page|
+ page.fill_key_title(title)
+ page.fill_key_value(key)
+
+ page.add_key
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/deploy_token.rb b/qa/qa/resource/deploy_token.rb
new file mode 100644
index 00000000000..cee4422f6b4
--- /dev/null
+++ b/qa/qa/resource/deploy_token.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+module QA
+ module Resource
+ class DeployToken < Base
+ attr_accessor :name, :expires_at
+
+ attribute :username do
+ Page::Project::Settings::Repository.perform do |page|
+ page.expand_deploy_tokens do |token|
+ token.token_username
+ end
+ end
+ end
+
+ attribute :password do
+ Page::Project::Settings::Repository.perform do |page|
+ page.expand_deploy_tokens do |token|
+ token.token_password
+ end
+ end
+ end
+
+ attribute :project do
+ Project.fabricate! do |resource|
+ resource.name = 'project-to-deploy'
+ resource.description = 'project for adding deploy token test'
+ end
+ end
+
+ def fabricate!
+ project.visit!
+
+ Page::Project::Menu.act do
+ click_repository_settings
+ end
+
+ Page::Project::Settings::Repository.perform do |setting|
+ setting.expand_deploy_tokens do |page|
+ page.fill_token_name(name)
+ page.fill_token_expires_at(expires_at)
+ page.fill_scopes(read_repository: true, read_registry: false)
+
+ page.add_token
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/file.rb b/qa/qa/resource/file.rb
new file mode 100644
index 00000000000..effc5a7940b
--- /dev/null
+++ b/qa/qa/resource/file.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module QA
+ module Resource
+ class File < Base
+ attr_accessor :name,
+ :content,
+ :commit_message
+
+ attribute :project do
+ Project.fabricate! do |resource|
+ resource.name = 'project-with-new-file'
+ end
+ end
+
+ def initialize
+ @name = 'QA Test - File name'
+ @content = 'QA Test - File content'
+ @commit_message = 'QA Test - Commit message'
+ end
+
+ def fabricate!
+ project.visit!
+
+ Page::Project::Show.perform(&:create_new_file!)
+
+ Page::File::Form.perform do |page|
+ page.add_name(@name)
+ page.add_content(@content)
+ page.add_commit_message(@commit_message)
+ page.commit_changes
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/fork.rb b/qa/qa/resource/fork.rb
new file mode 100644
index 00000000000..9fd66f3a36a
--- /dev/null
+++ b/qa/qa/resource/fork.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module QA
+ module Resource
+ class Fork < Base
+ attribute :push do
+ Repository::ProjectPush.fabricate!
+ end
+
+ attribute :user do
+ User.fabricate! do |resource|
+ if Runtime::Env.forker?
+ resource.username = Runtime::Env.forker_username
+ resource.password = Runtime::Env.forker_password
+ end
+ end
+ end
+
+ def fabricate!
+ populate(:push, :user)
+
+ # Sign out as admin and sign is as the fork user
+ Page::Main::Menu.perform(&:sign_out)
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.perform do |login|
+ login.sign_in_using_credentials(user)
+ end
+
+ push.project.visit!
+
+ Page::Project::Show.perform(&:fork_project)
+
+ Page::Project::Fork::New.perform do |fork_new|
+ fork_new.choose_namespace(user.name)
+ end
+
+ Page::Layout::Banner.perform do |page|
+ page.has_notice?('The project was successfully forked.')
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/group.rb b/qa/qa/resource/group.rb
new file mode 100644
index 00000000000..a7a6f931e28
--- /dev/null
+++ b/qa/qa/resource/group.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+module QA
+ module Resource
+ class Group < Base
+ attr_accessor :path, :description
+
+ attribute :sandbox do
+ Sandbox.fabricate!
+ end
+
+ attribute :id
+
+ def initialize
+ @path = Runtime::Namespace.name
+ @description = "QA test run at #{Runtime::Namespace.time}"
+ end
+
+ def fabricate!
+ sandbox.visit!
+
+ Page::Group::Show.perform do |group_show|
+ if group_show.has_subgroup?(path)
+ group_show.go_to_subgroup(path)
+ else
+ group_show.go_to_new_subgroup
+
+ Page::Group::New.perform do |group_new|
+ group_new.set_path(path)
+ group_new.set_description(description)
+ group_new.set_visibility('Public')
+ group_new.create
+ end
+
+ # Ensure that the group was actually created
+ group_show.wait(time: 1) do
+ group_show.has_text?(path) &&
+ group_show.has_new_project_or_subgroup_dropdown?
+ end
+ end
+ end
+ end
+
+ def fabricate_via_api!
+ resource_web_url(api_get)
+ rescue ResourceNotFoundError
+ super
+ end
+
+ def api_get_path
+ "/groups/#{CGI.escape("#{sandbox.path}/#{path}")}"
+ end
+
+ def api_members_path
+ "#{api_get_path}/members"
+ end
+
+ def api_post_path
+ '/groups'
+ end
+
+ def api_post_body
+ {
+ parent_id: sandbox.id,
+ path: path,
+ name: path,
+ visibility: 'public'
+ }
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/issue.rb b/qa/qa/resource/issue.rb
new file mode 100644
index 00000000000..2c2f27fe231
--- /dev/null
+++ b/qa/qa/resource/issue.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module QA
+ module Resource
+ class Issue < Base
+ attr_writer :description
+
+ attribute :project do
+ Project.fabricate! do |resource|
+ resource.name = 'project-for-issues'
+ resource.description = 'project for adding issues'
+ end
+ end
+
+ attribute :title
+
+ def fabricate!
+ project.visit!
+
+ Page::Project::Show.perform(&:go_to_new_issue)
+
+ Page::Project::Issue::New.perform do |page|
+ page.add_title(@title)
+ page.add_description(@description)
+ page.create_new_issue
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/kubernetes_cluster.rb b/qa/qa/resource/kubernetes_cluster.rb
new file mode 100644
index 00000000000..96c8843fb99
--- /dev/null
+++ b/qa/qa/resource/kubernetes_cluster.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require 'securerandom'
+
+module QA
+ module Resource
+ class KubernetesCluster < Base
+ attr_writer :project, :cluster,
+ :install_helm_tiller, :install_ingress, :install_prometheus, :install_runner
+
+ attribute :ingress_ip do
+ Page::Project::Operations::Kubernetes::Show.perform(&:ingress_ip)
+ end
+
+ def fabricate!
+ @project.visit!
+
+ Page::Project::Menu.perform(
+ &:click_operations_kubernetes)
+
+ Page::Project::Operations::Kubernetes::Index.perform(
+ &:add_kubernetes_cluster)
+
+ Page::Project::Operations::Kubernetes::Add.perform(
+ &:add_existing_cluster)
+
+ Page::Project::Operations::Kubernetes::AddExisting.perform do |page|
+ page.set_cluster_name(@cluster.cluster_name)
+ page.set_api_url(@cluster.api_url)
+ page.set_ca_certificate(@cluster.ca_certificate)
+ page.set_token(@cluster.token)
+ page.check_rbac! if @cluster.rbac
+ page.add_cluster!
+ end
+
+ if @install_helm_tiller
+ Page::Project::Operations::Kubernetes::Show.perform do |page|
+ # We must wait a few seconds for permissions to be set up correctly for new cluster
+ sleep 10
+
+ # Helm must be installed before everything else
+ page.install!(:helm)
+ page.await_installed(:helm)
+
+ page.install!(:ingress) if @install_ingress
+ page.install!(:prometheus) if @install_prometheus
+ page.install!(:runner) if @install_runner
+
+ page.await_installed(:ingress) if @install_ingress
+ page.await_installed(:prometheus) if @install_prometheus
+ page.await_installed(:runner) if @install_runner
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/label.rb b/qa/qa/resource/label.rb
new file mode 100644
index 00000000000..c0869cb1f2a
--- /dev/null
+++ b/qa/qa/resource/label.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require 'securerandom'
+
+module QA
+ module Resource
+ class Label < Base
+ attr_accessor :description, :color
+
+ attribute :title
+
+ attribute :project do
+ Project.fabricate! do |resource|
+ resource.name = 'project-with-label'
+ end
+ end
+
+ def initialize
+ @title = "qa-test-#{SecureRandom.hex(8)}"
+ @description = 'This is a test label'
+ @color = '#0033CC'
+ end
+
+ def fabricate!
+ project.visit!
+
+ Page::Project::Menu.perform(&:go_to_labels)
+ Page::Label::Index.perform(&:go_to_new_label)
+
+ Page::Label::New.perform do |page|
+ page.fill_title(@title)
+ page.fill_description(@description)
+ page.fill_color(@color)
+ page.create_label
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/merge_request.rb b/qa/qa/resource/merge_request.rb
new file mode 100644
index 00000000000..77afb3cfcba
--- /dev/null
+++ b/qa/qa/resource/merge_request.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+require 'securerandom'
+
+module QA
+ module Resource
+ class MergeRequest < Base
+ attr_accessor :title,
+ :description,
+ :source_branch,
+ :target_branch,
+ :assignee,
+ :milestone,
+ :labels,
+ :file_name,
+ :file_content
+
+ attribute :project do
+ Project.fabricate! do |resource|
+ resource.name = 'project-with-merge-request'
+ end
+ end
+
+ attribute :target do
+ project.visit!
+
+ Repository::ProjectPush.fabricate! do |resource|
+ resource.project = project
+ resource.branch_name = 'master'
+ resource.remote_branch = target_branch
+ end
+ end
+
+ attribute :source do
+ Repository::ProjectPush.fabricate! do |resource|
+ resource.project = project
+ resource.branch_name = target_branch
+ resource.remote_branch = source_branch
+ resource.new_branch = false
+ resource.file_name = file_name
+ resource.file_content = file_content
+ end
+ end
+
+ def initialize
+ @title = 'QA test - merge request'
+ @description = 'This is a test merge request'
+ @source_branch = "qa-test-feature-#{SecureRandom.hex(8)}"
+ @target_branch = "master"
+ @assignee = nil
+ @milestone = nil
+ @labels = []
+ @file_name = "added_file.txt"
+ @file_content = "File Added"
+ end
+
+ def fabricate!
+ populate(:target, :source)
+
+ project.visit!
+ Page::Project::Show.perform(&:new_merge_request)
+ Page::MergeRequest::New.perform do |page|
+ page.fill_title(@title)
+ page.fill_description(@description)
+ page.choose_milestone(@milestone) if @milestone
+ labels.each do |label|
+ page.select_label(label)
+ end
+
+ page.create_merge_request
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/merge_request_from_fork.rb b/qa/qa/resource/merge_request_from_fork.rb
new file mode 100644
index 00000000000..f91ae299d76
--- /dev/null
+++ b/qa/qa/resource/merge_request_from_fork.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module QA
+ module Resource
+ class MergeRequestFromFork < MergeRequest
+ attr_accessor :fork_branch
+
+ attribute :fork do
+ Fork.fabricate!
+ end
+
+ attribute :push do
+ Repository::ProjectPush.fabricate! do |resource|
+ resource.project = fork
+ resource.branch_name = fork_branch
+ resource.file_name = 'file2.txt'
+ resource.user = fork.user
+ end
+ end
+
+ def fabricate!
+ populate(:push)
+
+ fork.visit!
+
+ Page::Project::Show.perform(&:new_merge_request)
+ Page::MergeRequest::New.perform(&:create_merge_request)
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/personal_access_token.rb b/qa/qa/resource/personal_access_token.rb
new file mode 100644
index 00000000000..b8dd0a3562f
--- /dev/null
+++ b/qa/qa/resource/personal_access_token.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module QA
+ module Resource
+ ##
+ # Create a personal access token that can be used by the api
+ #
+ class PersonalAccessToken < Base
+ attr_accessor :name
+
+ attribute :access_token do
+ Page::Profile::PersonalAccessTokens.perform(&:created_access_token)
+ end
+
+ def fabricate!
+ Page::Main::Menu.perform(&:go_to_profile_settings)
+ Page::Profile::Menu.perform(&:click_access_tokens)
+
+ Page::Profile::PersonalAccessTokens.perform do |page|
+ page.fill_token_name(name || 'api-test-token')
+ page.check_api
+ page.create_token
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/project.rb b/qa/qa/resource/project.rb
new file mode 100644
index 00000000000..7fdf69278f9
--- /dev/null
+++ b/qa/qa/resource/project.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+require 'securerandom'
+
+module QA
+ module Resource
+ class Project < Base
+ attribute :name
+ attribute :description
+
+ attribute :group do
+ Group.fabricate!
+ end
+
+ attribute :repository_ssh_location do
+ Page::Project::Show.perform do |page|
+ page.choose_repository_clone_ssh
+ page.repository_location
+ end
+ end
+
+ attribute :repository_http_location do
+ Page::Project::Show.perform do |page|
+ page.choose_repository_clone_http
+ page.repository_location
+ end
+ end
+
+ def initialize
+ @description = 'My awesome project'
+ end
+
+ def name=(raw_name)
+ @name = "#{raw_name}-#{SecureRandom.hex(8)}"
+ end
+
+ def fabricate!
+ group.visit!
+
+ Page::Group::Show.perform(&:go_to_new_project)
+
+ Page::Project::New.perform do |page|
+ page.choose_test_namespace
+ page.choose_name(@name)
+ page.add_description(@description)
+ page.set_visibility('Public')
+ page.create_new_project
+ end
+ end
+
+ def api_get_path
+ "/projects/#{name}"
+ end
+
+ def api_post_path
+ '/projects'
+ end
+
+ def api_post_body
+ {
+ namespace_id: group.id,
+ path: name,
+ name: name,
+ description: description,
+ visibility: 'public'
+ }
+ end
+
+ private
+
+ def transform_api_resource(api_resource)
+ api_resource[:repository_ssh_location] =
+ Git::Location.new(api_resource[:ssh_url_to_repo])
+ api_resource[:repository_http_location] =
+ Git::Location.new(api_resource[:http_url_to_repo])
+ api_resource
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/project_imported_from_github.rb b/qa/qa/resource/project_imported_from_github.rb
new file mode 100644
index 00000000000..3f02fe885a9
--- /dev/null
+++ b/qa/qa/resource/project_imported_from_github.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require 'securerandom'
+
+module QA
+ module Resource
+ class ProjectImportedFromGithub < Project
+ attr_accessor :name
+ attr_writer :personal_access_token, :github_repository_path
+
+ attribute :group do
+ Group.fabricate!
+ end
+
+ def fabricate!
+ group.visit!
+
+ Page::Group::Show.perform(&:go_to_new_project)
+
+ Page::Project::New.perform do |page|
+ page.go_to_import_project
+ end
+
+ Page::Project::New.perform do |page|
+ page.go_to_github_import
+ end
+
+ Page::Project::Import::Github.perform do |page|
+ page.add_personal_access_token(@personal_access_token)
+ page.list_repos
+ page.import!(@github_repository_path, @name)
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/project_milestone.rb b/qa/qa/resource/project_milestone.rb
new file mode 100644
index 00000000000..a4d6657caff
--- /dev/null
+++ b/qa/qa/resource/project_milestone.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module QA
+ module Resource
+ class ProjectMilestone < Base
+ attr_reader :title
+ attr_accessor :description
+
+ attribute :project do
+ Project.fabricate!
+ end
+
+ def title=(title)
+ @title = "#{title}-#{SecureRandom.hex(4)}"
+ @description = 'A milestone'
+ end
+
+ def fabricate!
+ project.visit!
+
+ Page::Project::Menu.perform do |page|
+ page.click_issues
+ page.click_milestones
+ end
+
+ Page::Project::Milestone::Index.perform(&:click_new_milestone)
+
+ Page::Project::Milestone::New.perform do |milestone_new|
+ milestone_new.set_title(@title)
+ milestone_new.set_description(@description)
+ milestone_new.create_new_milestone
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/factory/repository/project_push.rb b/qa/qa/resource/repository/project_push.rb
index 167f47c9141..c9fafe3419f 100644
--- a/qa/qa/factory/repository/project_push.rb
+++ b/qa/qa/resource/repository/project_push.rb
@@ -1,18 +1,14 @@
+# frozen_string_literal: true
+
module QA
- module Factory
+ module Resource
module Repository
- class ProjectPush < Factory::Repository::Push
- dependency Factory::Resource::Project, as: :project do |project|
- project.name = 'project-with-code'
- project.description = 'Project with repository'
- end
-
- product :output do |factory|
- factory.output
- end
-
- product :project do |factory|
- factory.project
+ class ProjectPush < Repository::Push
+ attribute :project do
+ Project.fabricate! do |resource|
+ resource.name = 'project-with-code'
+ resource.description = 'Project with repository'
+ end
end
def initialize
diff --git a/qa/qa/factory/repository/push.rb b/qa/qa/resource/repository/push.rb
index 6c5088f1da5..c14d97ff7fb 100644
--- a/qa/qa/factory/repository/push.rb
+++ b/qa/qa/resource/repository/push.rb
@@ -1,9 +1,11 @@
+# frozen_string_literal: true
+
require 'pathname'
module QA
- module Factory
+ module Resource
module Repository
- class Push < Factory::Base
+ class Push < Base
attr_accessor :file_name, :file_content, :commit_message,
:branch_name, :new_branch, :output, :repository_http_uri,
:repository_ssh_uri, :ssh_key, :user
@@ -30,6 +32,14 @@ module QA
@directory = dir
end
+ def files=(files)
+ if !files.is_a?(Array) || files.empty?
+ raise ArgumentError, "Please provide an array of hashes e.g.: [{name: 'file1', content: 'foo'}]"
+ end
+
+ @files = files
+ end
+
def fabricate!
Git::Repository.perform do |repository|
if ssh_key
@@ -37,7 +47,7 @@ module QA
repository.use_ssh_key(ssh_key)
else
repository.uri = repository_http_uri
- repository.use_default_credentials
+ repository.use_default_credentials unless user
end
username = 'GitLab QA'
@@ -63,6 +73,10 @@ module QA
@directory.each_child do |f|
repository.add_file(f.basename, f.read) if f.file?
end
+ elsif @files
+ @files.each do |f|
+ repository.add_file(f[:name], f[:content])
+ end
else
repository.add_file(file_name, file_content)
end
diff --git a/qa/qa/factory/repository/wiki_push.rb b/qa/qa/resource/repository/wiki_push.rb
index ecc6cc18c88..f1c39d507fe 100644
--- a/qa/qa/factory/repository/wiki_push.rb
+++ b/qa/qa/resource/repository/wiki_push.rb
@@ -1,11 +1,15 @@
+# frozen_string_literal: true
+
module QA
- module Factory
+ module Resource
module Repository
- class WikiPush < Factory::Repository::Push
- dependency Factory::Resource::Wiki, as: :wiki do |wiki|
- wiki.title = 'Home'
- wiki.content = '# My First Wiki Content'
- wiki.message = 'Update home'
+ class WikiPush < Repository::Push
+ attribute :wiki do
+ Wiki.fabricate! do |resource|
+ resource.title = 'Home'
+ resource.content = '# My First Wiki Content'
+ resource.message = 'Update home'
+ end
end
def initialize
diff --git a/qa/qa/resource/runner.rb b/qa/qa/resource/runner.rb
new file mode 100644
index 00000000000..08ae3f22117
--- /dev/null
+++ b/qa/qa/resource/runner.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require 'securerandom'
+
+module QA
+ module Resource
+ class Runner < Base
+ attr_writer :name, :tags, :image
+
+ attribute :project do
+ Project.fabricate! do |resource|
+ resource.name = 'project-with-ci-cd'
+ resource.description = 'Project with CI/CD Pipelines'
+ end
+ end
+
+ def name
+ @name || "qa-runner-#{SecureRandom.hex(4)}"
+ end
+
+ def tags
+ @tags || %w[qa e2e]
+ end
+
+ def image
+ @image || 'gitlab/gitlab-runner:alpine'
+ end
+
+ def fabricate!
+ project.visit!
+
+ Page::Project::Menu.perform(&:click_ci_cd_settings)
+
+ Service::Runner.new(name).tap do |runner|
+ Page::Project::Settings::CICD.perform do |settings|
+ settings.expand_runners_settings do |runners|
+ runner.pull
+ runner.token = runners.registration_token
+ runner.address = runners.coordinator_address
+ runner.tags = tags
+ runner.image = image
+ runner.register!
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/sandbox.rb b/qa/qa/resource/sandbox.rb
new file mode 100644
index 00000000000..41ce857a8b8
--- /dev/null
+++ b/qa/qa/resource/sandbox.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+module QA
+ module Resource
+ ##
+ # Ensure we're in our sandbox namespace, either by navigating to it or by
+ # creating it if it doesn't yet exist.
+ #
+ class Sandbox < Base
+ attr_reader :path
+
+ attribute :id
+
+ def initialize
+ @path = Runtime::Namespace.sandbox_name
+ end
+
+ def fabricate!
+ Page::Main::Menu.perform(&:go_to_groups)
+
+ Page::Dashboard::Groups.perform do |page|
+ if page.has_group?(path)
+ page.go_to_group(path)
+ else
+ page.go_to_new_group
+
+ Page::Group::New.perform do |group|
+ group.set_path(path)
+ group.set_description('GitLab QA Sandbox Group')
+ group.set_visibility('Public')
+ group.create
+ end
+ end
+ end
+ end
+
+ def fabricate_via_api!
+ resource_web_url(api_get)
+ rescue ResourceNotFoundError
+ super
+ end
+
+ def api_get_path
+ "/groups/#{path}"
+ end
+
+ def api_post_path
+ '/groups'
+ end
+
+ def api_post_body
+ {
+ path: path,
+ name: path,
+ visibility: 'public'
+ }
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/settings/hashed_storage.rb b/qa/qa/resource/settings/hashed_storage.rb
new file mode 100644
index 00000000000..40c06768ffe
--- /dev/null
+++ b/qa/qa/resource/settings/hashed_storage.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module QA
+ module Resource
+ module Settings
+ class HashedStorage < Base
+ def fabricate!(*traits)
+ raise ArgumentError unless traits.include?(:enabled)
+
+ Page::Main::Login.perform(&:sign_in_using_credentials)
+ Page::Main::Menu.perform(&:go_to_admin_area)
+ Page::Admin::Menu.perform(&:go_to_repository_settings)
+
+ Page::Admin::Settings::Repository.perform do |setting|
+ setting.expand_repository_storage do |page|
+ page.enable_hashed_storage
+ page.save_settings
+ end
+ end
+
+ QA::Page::Main::Menu.perform(&:sign_out)
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/ssh_key.rb b/qa/qa/resource/ssh_key.rb
new file mode 100644
index 00000000000..c6c97c8532f
--- /dev/null
+++ b/qa/qa/resource/ssh_key.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module QA
+ module Resource
+ class SSHKey < Base
+ extend Forwardable
+
+ attr_accessor :title
+
+ def_delegators :key, :private_key, :public_key, :fingerprint
+
+ def key
+ @key ||= Runtime::Key::RSA.new
+ end
+
+ def fabricate!
+ Page::Main::Menu.perform(&:go_to_profile_settings)
+ Page::Profile::Menu.perform(&:click_ssh_keys)
+
+ Page::Profile::SSHKeys.perform do |page|
+ page.add_key(public_key, title)
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/user.rb b/qa/qa/resource/user.rb
new file mode 100644
index 00000000000..c26f0c84a1f
--- /dev/null
+++ b/qa/qa/resource/user.rb
@@ -0,0 +1,113 @@
+# frozen_string_literal: true
+
+require 'securerandom'
+
+module QA
+ module Resource
+ class User < Base
+ attr_reader :unique_id
+ attr_writer :username, :password, :name, :email
+ attr_accessor :provider, :extern_uid
+
+ def initialize
+ @unique_id = SecureRandom.hex(8)
+ end
+
+ def username
+ @username ||= "qa-user-#{unique_id}"
+ end
+
+ def password
+ @password ||= 'password'
+ end
+
+ def name
+ @name ||= username
+ end
+
+ def email
+ @email ||= "#{username}@example.com"
+ end
+
+ def credentials_given?
+ defined?(@username) && defined?(@password)
+ end
+
+ def fabricate!
+ # Don't try to log-out if we're not logged-in
+ if Page::Main::Menu.perform { |p| p.has_personal_area?(wait: 0) }
+ Page::Main::Menu.perform { |main| main.sign_out }
+ end
+
+ if credentials_given?
+ Page::Main::Login.perform do |login|
+ login.sign_in_using_credentials(self)
+ end
+ else
+ Page::Main::Login.perform do |login|
+ login.switch_to_register_tab
+ end
+ Page::Main::SignUp.perform do |signup|
+ signup.sign_up!(self)
+ end
+ end
+ end
+
+ def fabricate_via_api!
+ resource_web_url(api_get)
+ rescue ResourceNotFoundError
+ super
+ end
+
+ def api_get_path
+ "/users/#{fetch_id(username)}"
+ end
+
+ def api_post_path
+ '/users'
+ end
+
+ def api_post_body
+ {
+ email: email,
+ password: password,
+ username: username,
+ name: name,
+ skip_confirmation: true
+ }.merge(ldap_post_body)
+ end
+
+ def self.fabricate_or_use(username, password)
+ if Runtime::Env.signup_disabled?
+ self.new.tap do |user|
+ user.username = username
+ user.password = password
+ end
+ else
+ self.fabricate!
+ end
+ end
+
+ private
+
+ def ldap_post_body
+ return {} unless extern_uid && provider
+
+ {
+ extern_uid: extern_uid,
+ provider: provider
+ }
+ end
+
+ def fetch_id(username)
+ users = parse_body(api_get_from("/users?username=#{username}"))
+
+ unless users.size == 1 && users.first[:username] == username
+ raise ResourceNotFoundError, "Expected one user with username #{username} but found: `#{users}`."
+ end
+
+ users.first[:id]
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/wiki.rb b/qa/qa/resource/wiki.rb
new file mode 100644
index 00000000000..e942e9718a0
--- /dev/null
+++ b/qa/qa/resource/wiki.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module QA
+ module Resource
+ class Wiki < Base
+ attr_accessor :title, :content, :message
+
+ attribute :project do
+ Project.fabricate! do |resource|
+ resource.name = 'project-for-wikis'
+ resource.description = 'project for adding wikis'
+ end
+ end
+
+ def fabricate!
+ project.visit!
+
+ Page::Project::Menu.perform { |menu_side| menu_side.click_wiki }
+
+ Page::Project::Wiki::New.perform do |wiki_new|
+ wiki_new.go_to_create_first_page
+ wiki_new.set_title(@title)
+ wiki_new.set_content(@content)
+ wiki_new.set_message(@message)
+ wiki_new.create_new_page
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/runtime/api/client.rb b/qa/qa/runtime/api/client.rb
index 02015e23ad8..aff84c89f0e 100644
--- a/qa/qa/runtime/api/client.rb
+++ b/qa/qa/runtime/api/client.rb
@@ -6,33 +6,34 @@ module QA
class Client
attr_reader :address
- def initialize(address = :gitlab, personal_access_token: nil)
+ def initialize(address = :gitlab, personal_access_token: nil, is_new_session: true)
@address = address
@personal_access_token = personal_access_token
+ @is_new_session = is_new_session
end
def personal_access_token
- @personal_access_token ||= get_personal_access_token
- end
-
- def get_personal_access_token
- # you can set the environment variable PERSONAL_ACCESS_TOKEN
- # to use a specific access token rather than create one from the UI
- if Runtime::Env.personal_access_token
- Runtime::Env.personal_access_token
- else
- create_personal_access_token
+ @personal_access_token ||= begin
+ # you can set the environment variable PERSONAL_ACCESS_TOKEN
+ # to use a specific access token rather than create one from the UI
+ Runtime::Env.personal_access_token ||= create_personal_access_token
end
end
private
def create_personal_access_token
- Runtime::Browser.visit(@address, Page::Main::Login) do
- Page::Main::Login.act { sign_in_using_credentials }
- Factory::Resource::PersonalAccessToken.fabricate!.access_token
+ if @is_new_session
+ Runtime::Browser.visit(@address, Page::Main::Login) { do_create_personal_access_token }
+ else
+ do_create_personal_access_token
end
end
+
+ def do_create_personal_access_token
+ Page::Main::Login.act { sign_in_using_credentials }
+ Resource::PersonalAccessToken.fabricate!.access_token
+ end
end
end
end
diff --git a/qa/qa/runtime/browser.rb b/qa/qa/runtime/browser.rb
index 4c64270ce92..7fd2ba25527 100644
--- a/qa/qa/runtime/browser.rb
+++ b/qa/qa/runtime/browser.rb
@@ -51,6 +51,10 @@ module QA
}
)
+ if QA::Runtime::Env.accept_insecure_certs?
+ capabilities['acceptInsecureCerts'] = true
+ end
+
options = Selenium::WebDriver::Chrome::Options.new
options.add_argument("window-size=1240,1680")
@@ -113,6 +117,15 @@ module QA
def perform(&block)
visit(url)
+ if QA::Runtime::Env.qa_cookies
+ browser = Capybara.current_session.driver.browser
+ QA::Runtime::Env.qa_cookies.each do |cookie|
+ name, value = cookie.split("=")
+ value ||= ""
+ browser.manage.add_cookie name: name, value: value
+ end
+ end
+
yield.tap { clear! } if block_given?
end
diff --git a/qa/qa/runtime/env.rb b/qa/qa/runtime/env.rb
index 27ba915961d..3bc2b44ccd8 100644
--- a/qa/qa/runtime/env.rb
+++ b/qa/qa/runtime/env.rb
@@ -1,20 +1,54 @@
+# frozen_string_literal: true
+
module QA
module Runtime
module Env
extend self
+ attr_writer :personal_access_token, :ldap_username, :ldap_password
+
+ # The environment variables used to indicate if the environment under test
+ # supports the given feature
+ SUPPORTED_FEATURES = {
+ git_protocol_v2: 'QA_CAN_TEST_GIT_PROTOCOL_V2'
+ }.freeze
+
+ def supported_features
+ SUPPORTED_FEATURES
+ end
+
+ def debug?
+ enabled?(ENV['QA_DEBUG'], default: false)
+ end
+
+ def log_destination
+ ENV['QA_LOG_PATH'] || $stdout
+ end
+
# set to 'false' to have Chrome run visibly instead of headless
def chrome_headless?
- (ENV['CHROME_HEADLESS'] =~ /^(false|no|0)$/i) != 0
+ enabled?(ENV['CHROME_HEADLESS'])
+ end
+
+ def accept_insecure_certs?
+ enabled?(ENV['ACCEPT_INSECURE_CERTS'])
end
def running_in_ci?
ENV['CI'] || ENV['CI_SERVER']
end
+ def qa_cookies
+ ENV['QA_COOKIES'] && ENV['QA_COOKIES'].split(';')
+ end
+
+ def signup_disabled?
+ enabled?(ENV['SIGNUP_DISABLED'], default: false)
+ end
+
# specifies token that can be used for the api
def personal_access_token
- ENV['PERSONAL_ACCESS_TOKEN']
+ @personal_access_token ||= ENV['PERSONAL_ACCESS_TOKEN']
end
def user_username
@@ -34,7 +68,7 @@ module QA
end
def forker?
- forker_username && forker_password
+ !!(forker_username && forker_password)
end
def forker_username
@@ -45,18 +79,42 @@ module QA
ENV['GITLAB_FORKER_PASSWORD']
end
+ def gitlab_qa_username_1
+ ENV['GITLAB_QA_USERNAME_1'] || 'gitlab-qa-user1'
+ end
+
+ def gitlab_qa_password_1
+ ENV['GITLAB_QA_PASSWORD_1']
+ end
+
+ def gitlab_qa_username_2
+ ENV['GITLAB_QA_USERNAME_2'] || 'gitlab-qa-user2'
+ end
+
+ def gitlab_qa_password_2
+ ENV['GITLAB_QA_PASSWORD_2']
+ end
+
def ldap_username
- ENV['GITLAB_LDAP_USERNAME']
+ @ldap_username ||= ENV['GITLAB_LDAP_USERNAME']
end
def ldap_password
- ENV['GITLAB_LDAP_PASSWORD']
+ @ldap_password ||= ENV['GITLAB_LDAP_PASSWORD']
end
def sandbox_name
ENV['GITLAB_SANDBOX_NAME']
end
+ def namespace_name
+ ENV['GITLAB_NAMESPACE_NAME']
+ end
+
+ def auto_devops_project_name
+ ENV['GITLAB_AUTO_DEVOPS_PROJECT_NAME']
+ end
+
def gcloud_account_key
ENV.fetch("GCLOUD_ACCOUNT_KEY")
end
@@ -83,6 +141,23 @@ module QA
raise ArgumentError, "Please provide GITHUB_ACCESS_TOKEN"
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.
+ def can_test?(feature)
+ raise ArgumentError, %Q(Unknown feature "#{feature}") unless SUPPORTED_FEATURES.include? feature
+
+ enabled?(ENV[SUPPORTED_FEATURES[feature]], default: true)
+ end
+
+ private
+
+ def enabled?(value, default: true)
+ return default if value.nil?
+
+ (value =~ /^(false|no|0)$/i) != 0
+ end
end
end
end
diff --git a/qa/qa/runtime/fixtures.rb b/qa/qa/runtime/fixtures.rb
new file mode 100644
index 00000000000..72004d5b00a
--- /dev/null
+++ b/qa/qa/runtime/fixtures.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module QA
+ module Runtime
+ module Fixtures
+ def fetch_template_from_api(api_path, key)
+ request = Runtime::API::Request.new(api_client, "/templates/#{api_path}/#{key}")
+ get request.url
+ json_body[:content]
+ end
+
+ private
+
+ def api_client
+ @api_client ||= Runtime::API::Client.new(:gitlab)
+ end
+ end
+ end
+end
diff --git a/qa/qa/runtime/logger.rb b/qa/qa/runtime/logger.rb
new file mode 100644
index 00000000000..bd5c4fe5bf5
--- /dev/null
+++ b/qa/qa/runtime/logger.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'logger'
+
+module QA
+ module Runtime
+ module Logger
+ extend SingleForwardable
+
+ def_delegators :logger, :debug, :info, :warn, :error, :fatal, :unknown
+
+ singleton_class.module_eval do
+ 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
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/runtime/namespace.rb b/qa/qa/runtime/namespace.rb
index f1c8ef11f94..704c65467e0 100644
--- a/qa/qa/runtime/namespace.rb
+++ b/qa/qa/runtime/namespace.rb
@@ -8,7 +8,7 @@ module QA
end
def name
- "qa-test-#{time.strftime('%Y-%m-%d-%H-%M-%S')}"
+ Runtime::Env.namespace_name || "qa-test-#{time.strftime('%Y-%m-%d-%H-%M-%S')}"
end
def path
diff --git a/qa/qa/runtime/path.rb b/qa/qa/runtime/path.rb
new file mode 100644
index 00000000000..3169c5dd743
--- /dev/null
+++ b/qa/qa/runtime/path.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module QA
+ module Runtime
+ module Path
+ extend self
+
+ def qa_root
+ ::File.expand_path('../../', __dir__)
+ end
+ end
+ end
+end
diff --git a/qa/qa/scenario/test/integration/instance_saml.rb b/qa/qa/scenario/test/integration/instance_saml.rb
new file mode 100644
index 00000000000..0697d0c2a0e
--- /dev/null
+++ b/qa/qa/scenario/test/integration/instance_saml.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module QA
+ module Scenario
+ module Test
+ module Integration
+ class InstanceSAML < Test::Instance::All
+ tags :instance_saml
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/scenario/test/integration/ldap_no_tls.rb b/qa/qa/scenario/test/integration/ldap_no_tls.rb
new file mode 100644
index 00000000000..bbf4c847f33
--- /dev/null
+++ b/qa/qa/scenario/test/integration/ldap_no_tls.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module QA
+ module Scenario
+ module Test
+ module Integration
+ class LDAPNoTLS < Test::Instance::All
+ tags :ldap_no_tls
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/scenario/test/integration/ldap.rb b/qa/qa/scenario/test/integration/ldap_tls.rb
index 769fa389785..2a767e57bc6 100644
--- a/qa/qa/scenario/test/integration/ldap.rb
+++ b/qa/qa/scenario/test/integration/ldap_tls.rb
@@ -1,9 +1,11 @@
+# frozen_string_literal: true
+
module QA
module Scenario
module Test
module Integration
- class LDAP < Test::Instance::All
- tags :ldap
+ class LDAPTLS < Test::Instance::All
+ tags :ldap_tls
end
end
end
diff --git a/qa/qa/service/kubernetes_cluster.rb b/qa/qa/service/kubernetes_cluster.rb
index abd9d53554f..c5f12255d72 100644
--- a/qa/qa/service/kubernetes_cluster.rb
+++ b/qa/qa/service/kubernetes_cluster.rb
@@ -1,12 +1,17 @@
require 'securerandom'
require 'mkmf'
+require 'pathname'
module QA
module Service
class KubernetesCluster
include Service::Shellout
- attr_reader :api_url, :ca_certificate, :token
+ attr_reader :api_url, :ca_certificate, :token, :rbac
+
+ def initialize(rbac: false)
+ @rbac = rbac
+ end
def cluster_name
@cluster_name ||= "qa-cluster-#{SecureRandom.hex(4)}-#{Time.now.utc.strftime("%Y%m%d%H%M%S")}"
@@ -19,7 +24,8 @@ module QA
shell <<~CMD.tr("\n", ' ')
gcloud container clusters
create #{cluster_name}
- --enable-legacy-authorization
+ #{auth_options}
+ --enable-basic-auth
--zone #{Runtime::Env.gcloud_zone}
&& gcloud container clusters
get-credentials
@@ -28,8 +34,30 @@ module QA
CMD
@api_url = `kubectl config view --minify -o jsonpath='{.clusters[].cluster.server}'`
- @ca_certificate = Base64.decode64(`kubectl get secrets -o jsonpath="{.items[0].data['ca\\.crt']}"`)
- @token = Base64.decode64(`kubectl get secrets -o jsonpath='{.items[0].data.token}'`)
+
+ @admin_user = "#{cluster_name}-admin"
+ master_auth = JSON.parse(`gcloud container clusters describe #{cluster_name} --zone #{Runtime::Env.gcloud_zone} --format 'json(masterAuth.username, masterAuth.password)'`)
+ shell <<~CMD.tr("\n", ' ')
+ kubectl config set-credentials #{@admin_user}
+ --username #{master_auth['masterAuth']['username']}
+ --password #{master_auth['masterAuth']['password']}
+ CMD
+
+ if rbac
+ create_service_account
+
+ secrets = JSON.parse(`kubectl get secrets -o json`)
+ gitlab_account = secrets['items'].find do |item|
+ item['metadata']['annotations']['kubernetes.io/service-account.name'] == 'gitlab-account'
+ end
+
+ @ca_certificate = Base64.decode64(gitlab_account['data']['ca.crt'])
+ @token = Base64.decode64(gitlab_account['data']['token'])
+ else
+ @ca_certificate = Base64.decode64(`kubectl get secrets -o jsonpath="{.items[0].data['ca\\.crt']}"`)
+ @token = Base64.decode64(`kubectl get secrets -o jsonpath='{.items[0].data.token}'`)
+ end
+
self
end
@@ -44,6 +72,42 @@ module QA
private
+ def create_service_account
+ shell('kubectl create -f -', stdin_data: service_account)
+ shell("kubectl --user #{@admin_user} create -f -", stdin_data: service_account_role_binding)
+ end
+
+ def service_account
+ <<~YAML
+ apiVersion: v1
+ kind: ServiceAccount
+ metadata:
+ name: gitlab-account
+ namespace: default
+ YAML
+ end
+
+ def service_account_role_binding
+ <<~YAML
+ kind: ClusterRoleBinding
+ apiVersion: rbac.authorization.k8s.io/v1
+ metadata:
+ name: gitlab-account-binding
+ subjects:
+ - kind: ServiceAccount
+ name: gitlab-account
+ namespace: default
+ roleRef:
+ kind: ClusterRole
+ name: cluster-admin
+ apiGroup: rbac.authorization.k8s.io
+ YAML
+ end
+
+ def auth_options
+ "--enable-legacy-authorization" unless rbac
+ end
+
def validate_dependencies
find_executable('gcloud') || raise("You must first install `gcloud` executable to run these tests.")
find_executable('kubectl') || raise("You must first install `kubectl` executable to run these tests.")
diff --git a/qa/qa/service/shellout.rb b/qa/qa/service/shellout.rb
index 1ca9504bb33..43dc0851571 100644
--- a/qa/qa/service/shellout.rb
+++ b/qa/qa/service/shellout.rb
@@ -11,10 +11,12 @@ module QA
# TODO, make it possible to use generic QA framework classes
# as a library - gitlab-org/gitlab-qa#94
#
- def shell(command)
+ def shell(command, stdin_data: nil)
puts "Executing `#{command}`"
- Open3.popen2e(*command) do |_in, out, wait|
+ Open3.popen2e(*command) do |stdin, out, wait|
+ stdin.puts(stdin_data) if stdin_data
+ stdin.close if stdin_data
out.each { |line| puts line }
if wait.value.exited? && wait.value.exitstatus.nonzero?
diff --git a/qa/qa/specs/features/api/1_manage/users_spec.rb b/qa/qa/specs/features/api/1_manage/users_spec.rb
index 3e3c9e859aa..ba1ba204d24 100644
--- a/qa/qa/specs/features/api/1_manage/users_spec.rb
+++ b/qa/qa/specs/features/api/1_manage/users_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
- context :manage do
+ context 'Manage' do
describe 'Users API' do
before(:context) do
@api_client = Runtime::API::Client.new(:gitlab)
diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/log_in_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/log_in_spec.rb
index 1c7da930567..dae2a9e0236 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/login/log_in_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/login/log_in_spec.rb
@@ -1,5 +1,5 @@
module QA
- context :manage, :smoke do
+ context 'Manage', :smoke do
describe 'basic user login' do
it 'user logs in using basic credentials' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
@@ -8,7 +8,7 @@ module QA
# TODO, since `Signed in successfully` message was removed
# this is the only way to tell if user is signed in correctly.
#
- Page::Menu::Main.perform do |menu|
+ Page::Main::Menu.perform do |menu|
expect(menu).to have_personal_area
end
end
diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/log_into_gitlab_via_ldap_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/log_into_gitlab_via_ldap_spec.rb
index c296296def6..a397df03bd2 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/login/log_into_gitlab_via_ldap_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/login/log_into_gitlab_via_ldap_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
- context :manage, :orchestrated, :ldap do
+ context 'Manage', :orchestrated, :ldap_no_tls, :ldap_tls do
describe 'LDAP login' do
it 'user logs into GitLab using LDAP credentials' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
@@ -10,7 +10,7 @@ module QA
# TODO, since `Signed in successfully` message was removed
# this is the only way to tell if user is signed in correctly.
#
- Page::Menu::Main.perform do |menu|
+ Page::Main::Menu.perform do |menu|
expect(menu).to have_personal_area
end
end
diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/log_into_mattermost_via_gitlab_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/log_into_mattermost_via_gitlab_spec.rb
index 6eda2c750d4..b1d641b507f 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/login/log_into_mattermost_via_gitlab_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/login/log_into_mattermost_via_gitlab_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
- context :manage, :orchestrated, :mattermost do
+ context 'Manage', :orchestrated, :mattermost do
describe 'Mattermost login' do
it 'user logs into Mattermost using GitLab OAuth' do
Runtime::Browser.visit(:gitlab, Page::Main::Login) do
diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/login_via_instance_wide_saml_sso_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/login_via_instance_wide_saml_sso_spec.rb
new file mode 100644
index 00000000000..87f0e9030d2
--- /dev/null
+++ b/qa/qa/specs/features/browser_ui/1_manage/login/login_via_instance_wide_saml_sso_spec.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module QA
+ context 'Manage', :orchestrated, :instance_saml do
+ describe 'Instance wide SAML SSO' do
+ it 'User logs in to gitlab with SAML SSO' do
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+
+ Page::Main::Login.act { sign_in_with_saml }
+
+ Vendor::SAMLIdp::Page::Login.act { login }
+
+ expect(page).to have_content('Welcome to GitLab')
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb
new file mode 100644
index 00000000000..185837edacf
--- /dev/null
+++ b/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module QA
+ shared_examples 'registration and login' do
+ it 'user registers and logs in' do
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+
+ Resource::User.fabricate_via_browser_ui!
+
+ # TODO, since `Signed in successfully` message was removed
+ # this is the only way to tell if user is signed in correctly.
+ #
+ Page::Main::Menu.perform do |menu|
+ expect(menu).to have_personal_area
+ end
+ end
+ end
+
+ context 'Manage', :skip_signup_disabled do
+ describe 'standard' do
+ it_behaves_like 'registration and login'
+ end
+ end
+
+ context 'Manage', :orchestrated, :ldap_no_tls, :skip_signup_disabled do
+ describe 'while LDAP is enabled' do
+ it_behaves_like 'registration and login'
+ end
+ end
+end
diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb
new file mode 100644
index 00000000000..4070a225260
--- /dev/null
+++ b/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module QA
+ context 'Manage' do
+ describe 'Add project member' do
+ it 'user adds project member' do
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.perform(&:sign_in_using_credentials)
+
+ user = Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_1, Runtime::Env.gitlab_qa_password_1)
+
+ project = Resource::Project.fabricate! do |resource|
+ resource.name = 'add-member-project'
+ end
+ project.visit!
+
+ Page::Project::Menu.perform(&:click_members_settings)
+ Page::Project::Settings::Members.perform do |page|
+ page.add_member(user.username)
+ end
+
+ expect(page).to have_content("#{user.name} @#{user.username} Given access")
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb
index bb1f3ab26d1..6632c2977ef 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb
@@ -1,23 +1,21 @@
# frozen_string_literal: true
module QA
- context :manage, :smoke do
+ context 'Manage', :smoke do
describe 'Project creation' do
it 'user creates a new project' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- created_project = Factory::Resource::Project.fabricate! do |project|
+ created_project = Resource::Project.fabricate_via_browser_ui! do |project|
project.name = 'awesome-project'
project.description = 'create awesome project test'
end
- expect(created_project.name).to match /^awesome-project-\h{16}$/
-
+ expect(page).to have_content(created_project.name)
expect(page).to have_content(
/Project \S?awesome-project\S+ was successfully created/
)
-
expect(page).to have_content('create awesome project test')
expect(page).to have_content('The repository for this project is empty')
end
diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb
index 2ef8de61441..3ce48de2c25 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
@@ -1,10 +1,10 @@
# frozen_string_literal: true
module QA
- context :manage, :orchestrated, :github do
+ context 'Manage', :orchestrated, :github do
describe 'Project import from GitHub' do
let(:imported_project) do
- Factory::Resource::ProjectImportedFromGithub.fabricate! do |project|
+ Resource::ProjectImportedFromGithub.fabricate! do |project|
project.name = 'imported-project'
project.personal_access_token = Runtime::Env.github_access_token
project.github_repository_path = 'gitlab-qa/test-project'
@@ -27,7 +27,7 @@ module QA
imported_project # import the project
- Page::Menu::Main.act { go_to_projects }
+ Page::Main::Menu.act { go_to_projects }
Page::Dashboard::Projects.perform do |dashboard|
dashboard.go_to_project(imported_project.name)
end
@@ -48,7 +48,7 @@ module QA
end
def verify_issues_import
- Page::Menu::Side.act { click_issues }
+ Page::Project::Menu.act { click_issues }
expect(page).to have_content('This is a sample issue')
click_link 'This is a sample issue'
@@ -66,7 +66,7 @@ module QA
end
def verify_merge_requests_import
- Page::Menu::Side.act { click_merge_requests }
+ Page::Project::Menu.act { click_merge_requests }
expect(page).to have_content('Improve README.md')
click_link 'Improve README.md'
@@ -101,7 +101,7 @@ module QA
end
def verify_wiki_import
- Page::Menu::Side.act { click_wiki }
+ Page::Project::Menu.act { click_wiki }
expect(page).to have_content('Welcome to the test-project wiki!')
end
diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/view_project_activity_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/view_project_activity_spec.rb
index 34bb6f1c197..275de3d332c 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/project/view_project_activity_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/project/view_project_activity_spec.rb
@@ -1,19 +1,19 @@
# frozen_string_literal: true
module QA
- context :manage do
+ context 'Manage' do
describe 'Project activity' do
it 'user creates an event in the activity page upon Git push' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- Factory::Repository::ProjectPush.fabricate! do |push|
+ Resource::Repository::ProjectPush.fabricate! do |push|
push.file_name = 'README.md'
push.file_content = '# This is a test project'
push.commit_message = 'Add README.md'
end
- Page::Menu::Side.act { go_to_activity }
+ Page::Project::Menu.act { go_to_activity }
Page::Project::Activity.act { go_to_push_events }
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 542f532a629..7145b950b6c 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
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
- context :plan, :smoke do
+ context 'Plan', :smoke do
describe 'Issue creation' do
let(:issue_title) { 'issue title' }
@@ -9,7 +9,7 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- Factory::Resource::Issue.fabricate! do |issue|
+ Resource::Issue.fabricate! do |issue|
issue.title = issue_title
end
end
@@ -17,7 +17,7 @@ module QA
it 'user creates an issue' do
create_issue
- Page::Menu::Side.act { click_issues }
+ Page::Project::Menu.act { click_issues }
expect(page).to have_content(issue_title)
end
@@ -31,6 +31,7 @@ module QA
create_issue
Page::Project::Issue::Show.perform do |show|
+ show.select_all_activities_filter
show.comment('See attached banana for scale', attachment: file_to_attach)
show.refresh
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
new file mode 100644
index 00000000000..ac34f72bb8f
--- /dev/null
+++ b/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+module QA
+ context 'Plan' do
+ describe 'filter issue comments activities' do
+ let(:issue_title) { 'issue title' }
+
+ it 'user filters comments and activites in an issue' do
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.act { sign_in_using_credentials }
+
+ Resource::Issue.fabricate! do |issue|
+ issue.title = issue_title
+ end
+
+ expect(page).to have_content(issue_title)
+
+ Page::Project::Issue::Show.perform do |show_page|
+ show_page.select_comments_only_filter
+ show_page.comment('/confidential')
+ show_page.comment('My own comment')
+
+ expect(show_page).not_to have_content("made the issue confidential")
+ expect(show_page).to have_content("My own comment")
+
+ show_page.select_all_activities_filter
+
+ expect(show_page).to have_content("made the issue confidential")
+ expect(show_page).to have_content("My own comment")
+
+ show_page.select_history_only_filter
+
+ expect(show_page).to have_content("made the issue confidential")
+ expect(show_page).not_to have_content("My own comment")
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb
index bcf55a02a61..d33947f41da 100644
--- a/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb
@@ -1,31 +1,41 @@
# frozen_string_literal: true
module QA
- context :create do
+ context 'Create' do
describe 'Merge request creation' do
it 'user creates a new merge request' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- current_project = Factory::Resource::Project.fabricate! do |project|
+ current_project = Resource::Project.fabricate! do |project|
project.name = 'project-with-merge-request-and-milestone'
end
- current_milestone = Factory::Resource::ProjectMilestone.fabricate! do |milestone|
+ current_milestone = Resource::ProjectMilestone.fabricate! do |milestone|
milestone.title = 'unique-milestone'
milestone.project = current_project
end
- Factory::Resource::MergeRequest.fabricate! do |merge_request|
+ new_label = Resource::Label.fabricate! do |label|
+ label.project = current_project
+ label.title = 'qa-mr-test-label'
+ label.description = 'Merge Request label'
+ end
+
+ Resource::MergeRequest.fabricate! do |merge_request|
merge_request.title = 'This is a merge request with a milestone'
merge_request.description = 'Great feature with milestone'
merge_request.project = current_project
merge_request.milestone = current_milestone
+ merge_request.labels.push(new_label)
end
- expect(page).to have_content('This is a merge request with a milestone')
- expect(page).to have_content('Great feature with milestone')
- expect(page).to have_content(/Opened [\w\s]+ ago/)
+ Page::MergeRequest::Show.perform do |merge_request|
+ expect(merge_request).to have_content('This is a merge request with a milestone')
+ expect(merge_request).to have_content('Great feature with milestone')
+ expect(merge_request).to have_content(/Opened [\w\s]+ ago/)
+ expect(merge_request).to have_label(new_label.title)
+ end
Page::Issuable::Sidebar.perform do |sidebar|
expect(sidebar).to have_milestone(current_milestone.title)
@@ -39,11 +49,11 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- current_project = Factory::Resource::Project.fabricate! do |project|
+ current_project = Resource::Project.fabricate! do |project|
project.name = 'project-with-merge-request'
end
- Factory::Resource::MergeRequest.fabricate! do |merge_request|
+ Resource::MergeRequest.fabricate! do |merge_request|
merge_request.title = 'This is a merge request'
merge_request.description = 'Great feature'
merge_request.project = current_project
diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb
index 407a15800ab..6dcd74471fe 100644
--- a/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb
@@ -1,17 +1,17 @@
# frozen_string_literal: true
module QA
- context :create do
+ context 'Create' do
describe 'Merge request creation from fork' do
it 'user forks a project, submits a merge request and maintainer merges it' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- merge_request = Factory::Resource::MergeRequestFromFork.fabricate! do |merge_request|
+ merge_request = Resource::MergeRequestFromFork.fabricate! do |merge_request|
merge_request.fork_branch = 'feature-branch'
end
- Page::Menu::Main.perform { |main| main.sign_out }
+ Page::Main::Menu.perform { |main| main.sign_out }
Page::Main::Login.perform { |login| login.sign_in_using_credentials }
merge_request.visit!
diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb
index ddcbc94b1b1..e2d639fd150 100644
--- a/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb
@@ -1,25 +1,26 @@
# frozen_string_literal: true
module QA
- context :create do
+ context 'Create' do
describe 'Merge request rebasing' do
it 'user rebases source branch of merge request' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- project = Factory::Resource::Project.fabricate! do |project|
+ project = Resource::Project.fabricate! do |project|
project.name = "only-fast-forward"
end
+ project.visit!
- Page::Menu::Side.act { go_to_settings }
+ Page::Project::Menu.act { go_to_settings }
Page::Project::Settings::MergeRequest.act { enable_ff_only }
- merge_request = Factory::Resource::MergeRequest.fabricate! do |merge_request|
+ merge_request = Resource::MergeRequest.fabricate! do |merge_request|
merge_request.project = project
merge_request.title = 'Needs rebasing'
end
- Factory::Repository::ProjectPush.fabricate! do |push|
+ Resource::Repository::ProjectPush.fabricate! do |push|
push.project = project
push.file_name = "other.txt"
push.file_content = "New file added!"
diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb
index b5b8855a35d..6ff7360c413 100644
--- a/qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb
@@ -1,22 +1,22 @@
# frozen_string_literal: true
module QA
- context :create do
+ context 'Create' do
describe 'Merge request squashing' do
it 'user squashes commits while merging' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- project = Factory::Resource::Project.fabricate! do |project|
+ project = Resource::Project.fabricate! do |project|
project.name = "squash-before-merge"
end
- merge_request = Factory::Resource::MergeRequest.fabricate! do |merge_request|
+ merge_request = Resource::MergeRequest.fabricate! do |merge_request|
merge_request.project = project
merge_request.title = 'Squashing commits'
end
- Factory::Repository::ProjectPush.fabricate! do |push|
+ Resource::Repository::ProjectPush.fabricate! do |push|
push.project = project
push.commit_message = 'to be squashed'
push.branch_name = merge_request.source_branch
@@ -25,6 +25,7 @@ module QA
push.file_content = "Test with unicode characters ❤✓€❄"
end
+ Page::Project::Show.perform(&:wait_for_push)
merge_request.visit!
expect(page).to have_text('to be squashed')
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb
new file mode 100644
index 00000000000..297485dd81e
--- /dev/null
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+module QA
+ context 'Create' do
+ describe 'File templates' do
+ include Runtime::Fixtures
+
+ def login
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.act { sign_in_using_credentials }
+ end
+
+ before(:all) do
+ login
+
+ @project = Resource::Project.fabricate! do |project|
+ project.name = 'file-template-project'
+ project.description = 'Add file templates via the Files view'
+ end
+
+ Page::Main::Menu.act { sign_out }
+ end
+
+ templates = [
+ {
+ file_name: '.gitignore',
+ name: 'Android',
+ api_path: 'gitignores',
+ api_key: 'Android'
+ },
+ {
+ file_name: '.gitlab-ci.yml',
+ name: 'Julia',
+ api_path: 'gitlab_ci_ymls',
+ api_key: 'Julia'
+ },
+ {
+ file_name: 'Dockerfile',
+ name: 'Python',
+ api_path: 'dockerfiles',
+ api_key: 'Python'
+ },
+ {
+ file_name: 'LICENSE',
+ name: 'Mozilla Public License 2.0',
+ api_path: 'licenses',
+ api_key: 'mpl-2.0'
+ }
+ ]
+
+ templates.each do |template|
+ it "user adds #{template[:file_name]} via file template #{template[:name]}" do
+ content = fetch_template_from_api(template[:api_path], template[:api_key])
+
+ login
+ @project.visit!
+
+ Page::Project::Show.act { create_new_file! }
+ Page::File::Form.perform do |page|
+ page.select_template template[:file_name], template[:name]
+ end
+
+ expect(page).to have_content('Template applied')
+ expect(page).to have_button('Undo')
+ expect(page).to have_content(content[0..100])
+
+ Page::File::Form.perform(&:commit_changes)
+
+ expect(page).to have_content('The file has been successfully created.')
+ expect(page).to have_content(template[:file_name])
+ expect(page).to have_content('Add new file')
+ expect(page).to have_content(content[0..100])
+ end
+ end
+ end
+ 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 84f663c4866..ff879fdeb16 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
@@ -1,23 +1,23 @@
# frozen_string_literal: true
module QA
- context :create do
+ context 'Create' do
describe 'SSH keys support' do
let(:key_title) { "key for ssh tests #{Time.now.to_f}" }
- it 'user adds and then removes an SSH key' do
+ it 'user adds and then removes an SSH key', :smoke do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- key = Factory::Resource::SSHKey.fabricate! do |resource|
+ key = Resource::SSHKey.fabricate! do |resource|
resource.title = key_title
end
expect(page).to have_content("Title: #{key_title}")
expect(page).to have_content(key.fingerprint)
- Page::Menu::Main.act { go_to_profile_settings }
- Page::Menu::Profile.act { click_ssh_keys }
+ Page::Main::Menu.act { go_to_profile_settings }
+ Page::Profile::Menu.act { click_ssh_keys }
Page::Profile::SSHKeys.perform do |ssh_keys|
ssh_keys.remove_key(key_title)
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb
index 0dcdc6639d1..6a0add56fe0 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
module QA
- context :create do
- describe 'Git clone over HTTP', :ldap do
+ context 'Create' do
+ describe 'Git clone over HTTP', :ldap_no_tls do
let(:location) do
Page::Project::Show.act do
choose_repository_clone_http
@@ -14,10 +14,11 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- Factory::Resource::Project.fabricate! do |scenario|
+ project = Resource::Project.fabricate! do |scenario|
scenario.name = 'project-with-code'
scenario.description = 'project for git clone tests'
end
+ project.visit!
Git::Repository.perform do |repository|
repository.uri = location.uri
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/create_edit_delete_file_via_web_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/create_edit_delete_file_via_web_spec.rb
index 82d635065a0..46346d1b984 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/create_edit_delete_file_via_web_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/create_edit_delete_file_via_web_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
- context :create do
+ context 'Create' do
describe 'Files management' do
it 'user creates, edits and deletes a file via the Web' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
@@ -12,7 +12,7 @@ module QA
file_content = 'QA Test - File content'
commit_message_for_create = 'QA Test - Create new file'
- Factory::Resource::File.fabricate! do |file|
+ Resource::File.fabricate! do |file|
file.name = file_name
file.content = file_content
file.commit_message = commit_message_for_create
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_http_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_http_spec.rb
new file mode 100644
index 00000000000..43894372cf5
--- /dev/null
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_http_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+module QA
+ context 'Create' do
+ describe 'Push over HTTP using Git protocol version 2', :requires_git_protocol_v2 do
+ it 'user pushes to the repository' do
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.perform(&:sign_in_using_credentials)
+
+ # Create a project to push to
+ project = Resource::Project.fabricate! do |project|
+ project.name = 'git-protocol-project'
+ end
+
+ file_name = 'README.md'
+ file_content = 'Test Git protocol v2'
+ git_protocol = '2'
+ git_protocol_reported = nil
+
+ # Use Git to clone the project, push a file to it, and then check the
+ # supported Git protocol
+ Git::Repository.perform do |repository|
+ username = 'GitLab QA'
+ email = 'root@gitlab.com'
+
+ repository.uri = project.repository_http_location.uri
+ repository.use_default_credentials
+ repository.clone
+ repository.configure_identity(username, email)
+
+ git_protocol_reported = repository.push_with_git_protocol(
+ git_protocol,
+ file_name,
+ file_content)
+ end
+
+ project.visit!
+ Page::Project::Show.perform(&:wait_for_push)
+
+ # Check that the push worked
+ expect(page).to have_content(file_name)
+ expect(page).to have_content(file_content)
+
+ # And check that the correct Git protocol was used
+ expect(git_protocol_reported).to eq(git_protocol)
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_ssh_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_ssh_spec.rb
new file mode 100644
index 00000000000..135925c007f
--- /dev/null
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_ssh_spec.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+module QA
+ context 'Create' do
+ describe 'Push over SSH using Git protocol version 2', :requires_git_protocol_v2 do
+ # Note: If you run this test against GDK make sure you've enabled sshd and
+ # enabled setting the Git protocol by adding `AcceptEnv GIT_PROTOCOL` to
+ # `sshd_config`
+ # See: https://gitlab.com/gitlab-org/gitlab-qa/blob/master/docs/run_qa_against_gdk.md
+
+ let(:key_title) { "key for ssh tests #{Time.now.to_f}" }
+ let(:ssh_key) do
+ Resource::SSHKey.fabricate! do |resource|
+ resource.title = key_title
+ end
+ end
+
+ def login
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.perform(&:sign_in_using_credentials)
+ end
+
+ around do |example|
+ # Create an SSH key to be used with Git
+ login
+ ssh_key
+
+ example.run
+
+ # Remove the SSH key
+ login
+ Page::Main::Menu.perform(&:go_to_profile_settings)
+ Page::Profile::Menu.perform(&:click_ssh_keys)
+ Page::Profile::SSHKeys.perform do |ssh_keys|
+ ssh_keys.remove_key(key_title)
+ end
+ end
+
+ it 'user pushes to the repository' do
+ # Create a project to push to
+ project = Resource::Project.fabricate! do |project|
+ project.name = 'git-protocol-project'
+ end
+
+ file_name = 'README.md'
+ file_content = 'Test Git protocol v2'
+ git_protocol = '2'
+ git_protocol_reported = nil
+
+ # Use Git to clone the project, push a file to it, and then check the
+ # supported Git protocol
+ Git::Repository.perform do |repository|
+ username = 'GitLab QA'
+ email = 'root@gitlab.com'
+
+ repository.uri = project.repository_ssh_location.uri
+
+ begin
+ repository.use_ssh_key(ssh_key)
+ repository.clone
+ repository.configure_identity(username, email)
+
+ git_protocol_reported = repository.push_with_git_protocol(
+ git_protocol,
+ file_name,
+ file_content)
+ ensure
+ repository.delete_ssh_key
+ end
+ end
+
+ project.visit!
+ Page::Project::Show.perform(&:wait_for_push)
+
+ # Check that the push worked
+ expect(page).to have_content(file_name)
+ expect(page).to have_content(file_content)
+
+ # And check that the correct Git protocol was used
+ expect(git_protocol_reported).to eq(git_protocol)
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb
new file mode 100644
index 00000000000..a63b7dce8d6
--- /dev/null
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module QA
+ context 'Create' do
+ describe 'Git push over HTTP', :ldap_no_tls do
+ it 'user using a personal access token pushes code to the repository' do
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.perform(&:sign_in_using_credentials)
+
+ access_token = Resource::PersonalAccessToken.fabricate!.access_token
+
+ user = Resource::User.new.tap do |user|
+ user.username = Runtime::User.username
+ user.password = access_token
+ end
+
+ push = Resource::Repository::ProjectPush.fabricate! do |push|
+ push.user = user
+ push.file_name = 'README.md'
+ push.file_content = '# This is a test project'
+ push.commit_message = 'Add README.md'
+ end
+
+ push.project.visit!
+ Page::Project::Show.perform(&:wait_for_push)
+
+ expect(page).to have_content('README.md')
+ expect(page).to have_content('This is a test project')
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb
index bf32569b6cb..92f596a44d9 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb
@@ -1,13 +1,13 @@
# frozen_string_literal: true
module QA
- context :create do
- describe 'Git push over HTTP', :ldap do
+ context 'Create' do
+ describe 'Git push over HTTP', :ldap_no_tls do
it 'user pushes code to the repository' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- Factory::Repository::ProjectPush.fabricate! do |push|
+ Resource::Repository::ProjectPush.fabricate! do |push|
push.file_name = 'README.md'
push.file_content = '# This is a test project'
push.commit_message = 'Add README.md'
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb
index b2da685c477..73a3dc14a65 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb
@@ -1,12 +1,12 @@
# frozen_string_literal: true
module QA
- context :create do
- describe 'Protected branch support', :ldap do
+ context 'Create' do
+ describe 'Protected branch support', :ldap_no_tls do
let(:branch_name) { 'protected-branch' }
let(:commit_message) { 'Protected push commit message' }
let(:project) do
- Factory::Resource::Project.fabricate! do |resource|
+ Resource::Project.fabricate! do |resource|
resource.name = 'protected-branch-project'
end
end
@@ -47,7 +47,7 @@ module QA
end
def create_protected_branch(allow_to_push:)
- Factory::Resource::Branch.fabricate! do |resource|
+ Resource::Branch.fabricate! do |resource|
resource.branch_name = branch_name
resource.project = project
resource.allow_to_push = allow_to_push
@@ -56,7 +56,7 @@ module QA
end
def push_new_file(branch)
- Factory::Repository::ProjectPush.fabricate! do |resource|
+ Resource::Repository::ProjectPush.fabricate! do |resource|
resource.project = project
resource.file_name = 'new_file.md'
resource.file_content = '# This is a new file'
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/use_ssh_key_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/use_ssh_key_spec.rb
index 7c989bfd8cc..9c764424129 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/use_ssh_key_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/use_ssh_key_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
- context :create do
+ context 'Create' do
describe 'SSH key support' do
# Note: If you run this test against GDK make sure you've enabled sshd
# See: https://gitlab.com/gitlab-org/gitlab-qa/blob/master/docs/run_qa_against_gdk.md
@@ -12,11 +12,11 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- key = Factory::Resource::SSHKey.fabricate! do |resource|
+ key = Resource::SSHKey.fabricate! do |resource|
resource.title = key_title
end
- Factory::Repository::ProjectPush.fabricate! do |push|
+ Resource::Repository::ProjectPush.fabricate! do |push|
push.ssh_key = key
push.file_name = 'README.md'
push.file_content = '# Test Use SSH Key'
@@ -28,8 +28,8 @@ module QA
expect(page).to have_content('README.md')
expect(page).to have_content('Test Use SSH Key')
- Page::Menu::Main.act { go_to_profile_settings }
- Page::Menu::Profile.act { click_ssh_keys }
+ Page::Main::Menu.act { go_to_profile_settings }
+ Page::Profile::Menu.act { click_ssh_keys }
Page::Profile::SSHKeys.perform do |ssh_keys|
ssh_keys.remove_key(key_title)
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
new file mode 100644
index 00000000000..e7374377104
--- /dev/null
+++ b/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb
@@ -0,0 +1,88 @@
+# frozen_string_literal: true
+
+module QA
+ context 'Create' do
+ describe 'Web IDE file templates' do
+ include Runtime::Fixtures
+
+ def login
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.act { sign_in_using_credentials }
+ end
+
+ before(:all) do
+ login
+
+ @project = Resource::Project.fabricate! do |project|
+ project.name = 'file-template-project'
+ project.description = 'Add file templates via the Web IDE'
+ end
+ @project.visit!
+
+ # Add a file via the regular Files view because the Web IDE isn't
+ # available unless there is a file present
+ Page::Project::Show.act { create_new_file! }
+ Page::File::Form.perform do |page|
+ page.add_name('dummy')
+ page.add_content('Enable the Web IDE')
+ page.commit_changes
+ end
+
+ Page::Main::Menu.act { sign_out }
+ end
+
+ templates = [
+ {
+ file_name: '.gitignore',
+ name: 'Android',
+ api_path: 'gitignores',
+ api_key: 'Android'
+ },
+ {
+ file_name: '.gitlab-ci.yml',
+ name: 'Julia',
+ api_path: 'gitlab_ci_ymls',
+ api_key: 'Julia'
+ },
+ {
+ file_name: 'Dockerfile',
+ name: 'Python',
+ api_path: 'dockerfiles',
+ api_key: 'Python'
+ },
+ {
+ file_name: 'LICENSE',
+ name: 'Mozilla Public License 2.0',
+ api_path: 'licenses',
+ api_key: 'mpl-2.0'
+ }
+ ]
+
+ templates.each do |template|
+ it "user adds #{template[:file_name]} via file template #{template[:name]}" do
+ content = fetch_template_from_api(template[:api_path], template[:api_key])
+
+ login
+ @project.visit!
+
+ Page::Project::Show.act { open_web_ide! }
+ Page::Project::WebIDE::Edit.perform do |page|
+ page.create_new_file_from_template template[:file_name], template[:name]
+
+ expect(page.has_file?(template[:file_name])).to be_truthy
+ end
+
+ expect(page).to have_button('Undo')
+ expect(page).to have_content(content[0..100])
+
+ Page::Project::WebIDE::Edit.perform do |page|
+ page.commit_changes
+ end
+
+ expect(page).to have_content(template[:file_name])
+ expect(page).to have_content(content[0..100])
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb b/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb
index 8009b9e8609..210271705d9 100644
--- a/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
- context :create do
+ context 'Create' do
describe 'Wiki management' do
def login
Runtime::Browser.visit(:gitlab, Page::Main::Login)
@@ -18,7 +18,7 @@ module QA
end
it 'user creates, edits, clones, and pushes to the wiki' do
- wiki = Factory::Resource::Wiki.fabricate! do |resource|
+ wiki = Resource::Wiki.fabricate! do |resource|
resource.title = 'Home'
resource.content = '# My First Wiki Content'
resource.message = 'Update home'
@@ -34,13 +34,13 @@ module QA
validate_content('My Second Wiki Content')
- Factory::Repository::WikiPush.fabricate! do |push|
+ Resource::Repository::WikiPush.fabricate! do |push|
push.wiki = wiki
push.file_name = 'Home.md'
push.file_content = '# My Third Wiki Content'
push.commit_message = 'Update Home.md'
end
- Page::Menu::Side.act { click_wiki }
+ Page::Project::Menu.act { click_wiki }
expect(page).to have_content('My Third Wiki Content')
end
diff --git a/qa/qa/specs/features/browser_ui/4_verify/secret_variable/add_secret_variable_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_ci_variable_spec.rb
index 08a87df5837..0837b720df1 100644
--- a/qa/qa/specs/features/browser_ui/4_verify/secret_variable/add_secret_variable_spec.rb
+++ b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_ci_variable_spec.rb
@@ -1,25 +1,25 @@
# frozen_string_literal: true
module QA
- context :verify do
- describe 'Secret variable support' do
- it 'user adds a secret variable' do
+ context 'Verify' do
+ describe 'CI variable support' do
+ it 'user adds a CI variable' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- Factory::Resource::SecretVariable.fabricate! do |resource|
+ Resource::CiVariable.fabricate! do |resource|
resource.key = 'VARIABLE_KEY'
- resource.value = 'some secret variable'
+ resource.value = 'some CI variable'
end
Page::Project::Settings::CICD.perform do |settings|
- settings.expand_secret_variables do |page|
+ settings.expand_ci_variables do |page|
expect(page).to have_field(with: 'VARIABLE_KEY')
- expect(page).not_to have_field(with: 'some secret variable')
+ expect(page).not_to have_field(with: 'some CI variable')
page.reveal_variables
- expect(page).to have_field(with: 'some secret variable')
+ expect(page).to have_field(with: 'some CI variable')
end
end
end
diff --git a/qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb
index cdfe9b90e15..25cbe41c684 100644
--- a/qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb
+++ b/qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
- context :verify, :orchestrated, :docker do
+ context 'Verify', :orchestrated, :docker do
describe 'Pipeline creation and processing' do
let(:executor) { "qa-runner-#{Time.now.to_i}" }
@@ -13,18 +13,18 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- project = Factory::Resource::Project.fabricate! do |project|
+ project = Resource::Project.fabricate! do |project|
project.name = 'project-with-pipelines'
project.description = 'Project with CI/CD Pipelines.'
end
- Factory::Resource::Runner.fabricate! do |runner|
+ Resource::Runner.fabricate! do |runner|
runner.project = project
runner.name = executor
runner.tags = %w[qa test]
end
- Factory::Repository::ProjectPush.fabricate! do |push|
+ Resource::Repository::ProjectPush.fabricate! do |push|
push.project = project
push.file_name = '.gitlab-ci.yml'
push.commit_message = 'Add .gitlab-ci.yml'
@@ -64,7 +64,7 @@ module QA
expect(page).to have_content('Add .gitlab-ci.yml')
- Page::Menu::Side.act { click_ci_cd_pipelines }
+ Page::Project::Menu.act { click_ci_cd_pipelines }
expect(page).to have_content('All 1')
expect(page).to have_content('Add .gitlab-ci.yml')
diff --git a/qa/qa/specs/features/browser_ui/4_verify/runner/register_runner_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/runner/register_runner_spec.rb
index 8d83a20f5bf..3af7db751e7 100644
--- a/qa/qa/specs/features/browser_ui/4_verify/runner/register_runner_spec.rb
+++ b/qa/qa/specs/features/browser_ui/4_verify/runner/register_runner_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
- context :verify, :docker do
+ context 'Verify', :docker do
describe 'Runner registration' do
let(:executor) { "qa-runner-#{Time.now.to_i}" }
@@ -13,7 +13,7 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- Factory::Resource::Runner.fabricate! do |runner|
+ Resource::Runner.fabricate! do |runner|
runner.name = executor
end
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 17dfa887434..84757f25379 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
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
- context :release do
+ context 'Release' do
describe 'Deploy key creation' do
it 'user adds a deploy key' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
@@ -11,7 +11,7 @@ module QA
deploy_key_title = 'deploy key title'
deploy_key_value = key.public_key
- deploy_key = Factory::Resource::DeployKey.fabricate! do |resource|
+ deploy_key = Resource::DeployKey.fabricate! do |resource|
resource.title = deploy_key_title
resource.key = deploy_key_value
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 8352d13b06d..e2320c92343 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,7 +3,7 @@
require 'digest/sha1'
module QA
- context :release, :docker do
+ context 'Release', :docker do
describe 'Git clone using a deploy key' do
def login
Runtime::Browser.visit(:gitlab, Page::Main::Login)
@@ -15,20 +15,20 @@ module QA
@runner_name = "qa-runner-#{Time.now.to_i}"
- @project = Factory::Resource::Project.fabricate! do |resource|
+ @project = Resource::Project.fabricate! do |resource|
resource.name = 'deploy-key-clone-project'
end
@repository_location = @project.repository_ssh_location
- Factory::Resource::Runner.fabricate! do |resource|
+ Resource::Runner.fabricate! do |resource|
resource.project = @project
resource.name = @runner_name
resource.tags = %w[qa docker]
resource.image = 'gitlab/gitlab-runner:ubuntu'
end
- Page::Menu::Main.act { sign_out }
+ Page::Main::Menu.act { sign_out }
end
after(:all) do
@@ -47,7 +47,7 @@ module QA
login
- Factory::Resource::DeployKey.fabricate! do |resource|
+ Resource::DeployKey.fabricate! do |resource|
resource.project = @project
resource.title = "deploy key #{key.name}(#{key.bits})"
resource.key = key.public_key
@@ -55,7 +55,7 @@ module QA
deploy_key_name = "DEPLOY_KEY_#{key.name}_#{key.bits}"
- Factory::Resource::SecretVariable.fabricate! do |resource|
+ Resource::CiVariable.fabricate! do |resource|
resource.project = @project
resource.key = deploy_key_name
resource.value = key.private_key
@@ -78,7 +78,7 @@ module QA
- docker
YAML
- Factory::Repository::ProjectPush.fabricate! do |resource|
+ Resource::Repository::ProjectPush.fabricate! do |resource|
resource.project = @project
resource.file_name = '.gitlab-ci.yml'
resource.commit_message = 'Add .gitlab-ci.yml'
@@ -90,7 +90,7 @@ module QA
sha1sum = Digest::SHA1.hexdigest(gitlab_ci)
Page::Project::Show.act { wait_for_push }
- Page::Menu::Side.act { click_ci_cd_pipelines }
+ Page::Project::Menu.act { click_ci_cd_pipelines }
Page::Project::Pipeline::Index.act { go_to_latest_pipeline }
Page::Project::Pipeline::Show.act { go_to_first_job }
diff --git a/qa/qa/specs/features/browser_ui/6_release/deploy_token/add_deploy_token_spec.rb b/qa/qa/specs/features/browser_ui/6_release/deploy_token/add_deploy_token_spec.rb
new file mode 100644
index 00000000000..9f34e4218c1
--- /dev/null
+++ b/qa/qa/specs/features/browser_ui/6_release/deploy_token/add_deploy_token_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module QA
+ context 'Release' do
+ describe 'Deploy token creation' do
+ it 'user adds a deploy token' do
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.act { sign_in_using_credentials }
+
+ deploy_token_name = 'deploy token name'
+ deploy_token_expires_at = Date.today + 7 # 1 Week from now
+
+ deploy_token = Resource::DeployToken.fabricate! do |resource|
+ resource.name = deploy_token_name
+ resource.expires_at = deploy_token_expires_at
+ end
+
+ expect(deploy_token.username.length).to be > 0
+ expect(deploy_token.password.length).to be > 0
+ end
+ end
+ end
+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 844cc1236c7..b0c277a48c3 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
@@ -3,65 +3,81 @@
require 'pathname'
module QA
- context :configure, :orchestrated, :kubernetes do
+ context 'Configure', :orchestrated, :kubernetes do
describe 'Auto DevOps support' do
after do
@cluster&.remove!
end
- it 'user creates a new project and runs auto devops' do
- Runtime::Browser.visit(:gitlab, Page::Main::Login)
- Page::Main::Login.act { sign_in_using_credentials }
+ [true, false].each do |rbac|
+ context "when rbac is #{rbac ? 'enabled' : 'disabled'}" do
+ it 'user creates a new project and runs auto devops' do
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.act { sign_in_using_credentials }
- project = Factory::Resource::Project.fabricate! do |p|
- p.name = 'project-with-autodevops'
- p.description = 'Project with Auto Devops'
- end
+ project = Resource::Project.fabricate! do |p|
+ p.name = Runtime::Env.auto_devops_project_name || 'project-with-autodevops'
+ p.description = 'Project with Auto Devops'
+ end
- # Disable code_quality check in Auto DevOps pipeline as it takes
- # too long and times out the test
- Factory::Resource::SecretVariable.fabricate! do |resource|
- resource.project = project
- resource.key = 'CODE_QUALITY_DISABLED'
- resource.value = '1'
- end
+ # Disable code_quality check in Auto DevOps pipeline as it takes
+ # too long and times out the test
+ Resource::CiVariable.fabricate! do |resource|
+ resource.project = project
+ resource.key = 'CODE_QUALITY_DISABLED'
+ resource.value = '1'
+ end
- # Create Auto Devops compatible repo
- Factory::Repository::ProjectPush.fabricate! do |push|
- push.project = project
- push.directory = Pathname
- .new(__dir__)
- .join('../../../../../fixtures/auto_devops_rack')
- push.commit_message = 'Create Auto DevOps compatible rack application'
- end
+ # Create Auto Devops compatible repo
+ Resource::Repository::ProjectPush.fabricate! do |push|
+ push.project = project
+ push.directory = Pathname
+ .new(__dir__)
+ .join('../../../../../fixtures/auto_devops_rack')
+ push.commit_message = 'Create Auto DevOps compatible rack application'
+ end
- Page::Project::Show.act { wait_for_push }
+ Page::Project::Show.act { wait_for_push }
- # Create and connect K8s cluster
- @cluster = Service::KubernetesCluster.new.create!
- kubernetes_cluster = Factory::Resource::KubernetesCluster.fabricate! do |cluster|
- cluster.project = project
- cluster.cluster = @cluster
- cluster.install_helm_tiller = true
- cluster.install_ingress = true
- cluster.install_prometheus = true
- cluster.install_runner = true
- end
+ # Create and connect K8s cluster
+ @cluster = Service::KubernetesCluster.new(rbac: rbac).create!
+ kubernetes_cluster = Resource::KubernetesCluster.fabricate! do |cluster|
+ cluster.project = project
+ cluster.cluster = @cluster
+ cluster.install_helm_tiller = true
+ cluster.install_ingress = true
+ cluster.install_prometheus = true
+ cluster.install_runner = true
+ end
+ kubernetes_cluster.populate(:ingress_ip)
- project.visit!
- Page::Menu::Side.act { click_ci_cd_settings }
- Page::Project::Settings::CICD.perform do |p|
- p.enable_auto_devops_with_domain("#{kubernetes_cluster.ingress_ip}.nip.io")
- end
+ project.visit!
+ Page::Project::Menu.act { click_ci_cd_settings }
+ Page::Project::Settings::CICD.perform do |p|
+ p.enable_auto_devops_with_domain(
+ "#{kubernetes_cluster.ingress_ip}.nip.io")
+ end
+
+ project.visit!
+ Page::Project::Menu.act { click_ci_cd_pipelines }
+ Page::Project::Pipeline::Index.act { go_to_latest_pipeline }
- project.visit!
- Page::Menu::Side.act { click_ci_cd_pipelines }
- Page::Project::Pipeline::Index.act { go_to_latest_pipeline }
+ Page::Project::Pipeline::Show.perform do |pipeline|
+ expect(pipeline).to have_build('build', status: :success, wait: 600)
+ expect(pipeline).to have_build('test', status: :success, wait: 600)
+ expect(pipeline).to have_build('production', status: :success, wait: 1200)
+ end
- Page::Project::Pipeline::Show.perform do |pipeline|
- expect(pipeline).to have_build('build', status: :success, wait: 600)
- expect(pipeline).to have_build('test', status: :success, wait: 600)
- expect(pipeline).to have_build('production', status: :success, wait: 1200)
+ Page::Project::Menu.act { click_operations_environments }
+ Page::Project::Operations::Environments::Index.perform do |index|
+ index.go_to_environment('production')
+ end
+ Page::Project::Operations::Environments::Show.perform do |show|
+ show.view_deployment do
+ expect(page).to have_content('Hello World!')
+ end
+ end
+ end
end
end
end
diff --git a/qa/qa/specs/features/browser_ui/7_configure/mattermost/create_group_with_mattermost_team_spec.rb b/qa/qa/specs/features/browser_ui/7_configure/mattermost/create_group_with_mattermost_team_spec.rb
index 6ffdc55538a..7096864e011 100644
--- a/qa/qa/specs/features/browser_ui/7_configure/mattermost/create_group_with_mattermost_team_spec.rb
+++ b/qa/qa/specs/features/browser_ui/7_configure/mattermost/create_group_with_mattermost_team_spec.rb
@@ -1,12 +1,12 @@
# frozen_string_literal: true
module QA
- context :configure, :orchestrated, :mattermost do
+ context 'Configure', :orchestrated, :mattermost do
describe 'Mattermost support' do
it 'user creates a group with a mattermost team' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- Page::Menu::Main.act { go_to_groups }
+ Page::Main::Menu.act { go_to_groups }
Page::Dashboard::Groups.perform do |page|
page.go_to_new_group
diff --git a/qa/qa/specs/features/sanity/framework_spec.rb b/qa/qa/specs/features/sanity/framework_spec.rb
index ee9d068eb3a..aae0f0ade71 100644
--- a/qa/qa/specs/features/sanity/framework_spec.rb
+++ b/qa/qa/specs/features/sanity/framework_spec.rb
@@ -6,9 +6,7 @@ module QA
it 'succeeds' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
- Page::Main::Login.perform do |main_login|
- expect(main_login.sign_in_tab?).to be(true)
- end
+ expect(page).to have_text('Open source software to collaborate on code')
end
end
diff --git a/qa/qa/specs/runner.rb b/qa/qa/specs/runner.rb
index fea0ef94df3..1bd8101c36d 100644
--- a/qa/qa/specs/runner.rb
+++ b/qa/qa/specs/runner.rb
@@ -23,6 +23,12 @@ module QA
args.push(%w[--tag ~orchestrated]) unless (%w[-t --tag] & options).any?
end
+ args.push(%w[--tag ~skip_signup_disabled]) if QA::Runtime::Env.signup_disabled?
+
+ QA::Runtime::Env.supported_features.each_key do |key|
+ args.push(["--tag", "~requires_#{key}"]) unless QA::Runtime::Env.can_test? key
+ end
+
args.push(options)
args.push(DEFAULT_TEST_PATH_ARGS) unless options.any? { |opt| opt =~ %r{/features/} }
diff --git a/qa/qa/support/api.rb b/qa/qa/support/api.rb
new file mode 100644
index 00000000000..1107d43161e
--- /dev/null
+++ b/qa/qa/support/api.rb
@@ -0,0 +1,28 @@
+module QA
+ module Support
+ module Api
+ def post(url, payload)
+ RestClient::Request.execute(
+ method: :post,
+ url: url,
+ payload: payload,
+ verify_ssl: false)
+ rescue RestClient::ExceptionWithResponse => e
+ e.response
+ end
+
+ def get(url)
+ RestClient::Request.execute(
+ method: :get,
+ url: url,
+ verify_ssl: false)
+ rescue RestClient::ExceptionWithResponse => e
+ e.response
+ end
+
+ def parse_body(response)
+ JSON.parse(response.body, symbolize_names: true)
+ end
+ end
+ end
+end
diff --git a/qa/qa/support/page/logging.rb b/qa/qa/support/page/logging.rb
new file mode 100644
index 00000000000..cf5cd3a79f8
--- /dev/null
+++ b/qa/qa/support/page/logging.rb
@@ -0,0 +1,100 @@
+# frozen_string_literal: true
+
+module QA
+ module Support
+ module Page
+ module Logging
+ def refresh
+ log("refreshing #{current_url}")
+
+ super
+ end
+
+ def wait(max: 60, time: 0.1, reload: true)
+ log("with wait: max #{max}; time #{time}; reload #{reload}")
+ now = Time.now
+
+ element = super
+
+ log("ended wait after #{Time.now - now} seconds")
+
+ element
+ end
+
+ def scroll_to(selector, text: nil)
+ msg = "scrolling to :#{selector}"
+ msg += " with text: #{text}" if text
+ log(msg)
+
+ super
+ end
+
+ def asset_exists?(url)
+ exists = super
+
+ log("asset_exists? #{url} returned #{exists}")
+
+ exists
+ end
+
+ def find_element(name)
+ log("finding :#{name}")
+
+ element = super
+
+ log("found :#{name}") if element
+
+ element
+ end
+
+ def all_elements(name)
+ log("finding all :#{name}")
+
+ elements = super
+
+ log("found #{elements.size} :#{name}") if elements
+
+ elements
+ end
+
+ def click_element(name)
+ log("clicking :#{name}")
+
+ super
+ end
+
+ def fill_element(name, content)
+ masked_content = name.to_s.include?('password') ? '*****' : content
+
+ log(%Q(filling :#{name} with "#{masked_content}"))
+
+ super
+ end
+
+ def has_element?(name)
+ found = super
+
+ log("has_element? :#{name} returned #{found}")
+
+ found
+ end
+
+ def within_element(name)
+ log("within element :#{name}")
+
+ element = super
+
+ log("end within element :#{name}")
+
+ element
+ end
+
+ private
+
+ def log(msg)
+ QA::Runtime::Logger.debug(msg)
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/vendor/saml_idp/page/base.rb b/qa/qa/vendor/saml_idp/page/base.rb
new file mode 100644
index 00000000000..286cb0a8cd8
--- /dev/null
+++ b/qa/qa/vendor/saml_idp/page/base.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module QA
+ module Vendor
+ module SAMLIdp
+ module Page
+ class Base
+ include Capybara::DSL
+ include Scenario::Actable
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/vendor/saml_idp/page/login.rb b/qa/qa/vendor/saml_idp/page/login.rb
new file mode 100644
index 00000000000..9c1f9904a7a
--- /dev/null
+++ b/qa/qa/vendor/saml_idp/page/login.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require 'capybara/dsl'
+
+module QA
+ module Vendor
+ module SAMLIdp
+ module Page
+ class Login < Page::Base
+ def login
+ fill_in 'username', with: 'user1'
+ fill_in 'password', with: 'user1pass'
+ click_on 'Login'
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/spec/factory/base_spec.rb b/qa/spec/factory/base_spec.rb
deleted file mode 100644
index 04e04886699..00000000000
--- a/qa/spec/factory/base_spec.rb
+++ /dev/null
@@ -1,132 +0,0 @@
-describe QA::Factory::Base do
- let(:factory) { spy('factory') }
- let(:product) { spy('product') }
-
- describe '.fabricate!' do
- subject { Class.new(described_class) }
-
- before do
- allow(QA::Factory::Product).to receive(:new).and_return(product)
- allow(QA::Factory::Product).to receive(:populate!).and_return(product)
- end
-
- it 'instantiates the factory and calls factory method' do
- expect(subject).to receive(:new).and_return(factory)
-
- subject.fabricate!('something')
-
- expect(factory).to have_received(:fabricate!).with('something')
- end
-
- it 'returns fabrication product' do
- allow(subject).to receive(:new).and_return(factory)
-
- result = subject.fabricate!('something')
-
- expect(result).to eq product
- end
-
- it 'yields factory before calling factory method' do
- allow(subject).to receive(:new).and_return(factory)
-
- subject.fabricate! do |factory|
- factory.something!
- end
-
- expect(factory).to have_received(:something!).ordered
- expect(factory).to have_received(:fabricate!).ordered
- end
- end
-
- describe '.dependency' do
- let(:dependency) { spy('dependency') }
-
- before do
- stub_const('Some::MyDependency', dependency)
- end
-
- subject do
- Class.new(described_class) do
- dependency Some::MyDependency, as: :mydep do |factory|
- factory.something!
- end
- end
- end
-
- it 'appends a new dependency and accessors' do
- expect(subject.dependencies).to be_one
- end
-
- it 'defines dependency accessors' do
- expect(subject.new).to respond_to :mydep, :mydep=
- end
-
- describe 'dependencies fabrication' do
- let(:dependency) { double('dependency') }
- let(:instance) { spy('instance') }
-
- subject do
- Class.new(described_class) do
- dependency Some::MyDependency, as: :mydep
- end
- end
-
- before do
- stub_const('Some::MyDependency', dependency)
-
- allow(subject).to receive(:new).and_return(instance)
- allow(instance).to receive(:mydep).and_return(nil)
- allow(QA::Factory::Product).to receive(:new)
- allow(QA::Factory::Product).to receive(:populate!)
- end
-
- it 'builds all dependencies first' do
- expect(dependency).to receive(:fabricate!).once
-
- subject.fabricate!
- end
- end
- end
-
- describe '.product' do
- subject do
- Class.new(described_class) do
- def fabricate!
- "any"
- end
-
- # Defined only to be stubbed
- def self.find_page
- end
-
- product :token do
- find_page.do_something_on_page!
- 'resulting value'
- end
- end
- end
-
- it 'appends new product attribute' do
- expect(subject.attributes).to be_one
- expect(subject.attributes).to have_key(:token)
- end
-
- describe 'populating fabrication product with data' do
- let(:page) { spy('page') }
-
- before do
- allow(factory).to receive(:class).and_return(subject)
- allow(QA::Factory::Product).to receive(:new).and_return(product)
- allow(product).to receive(:page).and_return(page)
- allow(subject).to receive(:find_page).and_return(page)
- end
-
- it 'populates product after fabrication' do
- subject.fabricate!
-
- expect(product.token).to eq 'resulting value'
- expect(page).to have_received(:do_something_on_page!)
- end
- end
- end
-end
diff --git a/qa/spec/factory/dependency_spec.rb b/qa/spec/factory/dependency_spec.rb
deleted file mode 100644
index 8aaa6665a18..00000000000
--- a/qa/spec/factory/dependency_spec.rb
+++ /dev/null
@@ -1,72 +0,0 @@
-describe QA::Factory::Dependency do
- let(:dependency) { spy('dependency' ) }
- let(:factory) { spy('factory') }
- let(:block) { spy('block') }
-
- let(:signature) do
- double('signature', factory: dependency, block: block)
- end
-
- subject do
- described_class.new(:mydep, factory, signature)
- end
-
- describe '#overridden?' do
- it 'returns true if factory has overridden dependency' do
- allow(factory).to receive(:mydep).and_return('something')
-
- expect(subject).to be_overridden
- end
-
- it 'returns false if dependency has not been overridden' do
- allow(factory).to receive(:mydep).and_return(nil)
-
- expect(subject).not_to be_overridden
- end
- end
-
- describe '#build!' do
- context 'when dependency has been overridden' do
- before do
- allow(subject).to receive(:overridden?).and_return(true)
- end
-
- it 'does not fabricate dependency' do
- subject.build!
-
- expect(dependency).not_to have_received(:fabricate!)
- end
- end
-
- context 'when dependency has not been overridden' do
- before do
- allow(subject).to receive(:overridden?).and_return(false)
- end
-
- it 'fabricates dependency' do
- subject.build!
-
- expect(dependency).to have_received(:fabricate!)
- end
-
- it 'sets product in the factory' do
- subject.build!
-
- expect(factory).to have_received(:mydep=).with(dependency)
- end
-
- context 'when receives a caller factory as block argument' do
- let(:dependency) { QA::Factory::Base }
-
- it 'calls given block with dependency factory and caller factory' do
- allow_any_instance_of(QA::Factory::Base).to receive(:fabricate!).and_return(factory)
- allow(QA::Factory::Product).to receive(:populate!).and_return(spy('any'))
-
- subject.build!
-
- expect(block).to have_received(:call).with(an_instance_of(QA::Factory::Base), factory)
- end
- end
- end
- end
-end
diff --git a/qa/spec/factory/product_spec.rb b/qa/spec/factory/product_spec.rb
deleted file mode 100644
index f245aabbf43..00000000000
--- a/qa/spec/factory/product_spec.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-describe QA::Factory::Product do
- let(:factory) do
- QA::Factory::Base.new
- end
-
- let(:attributes) do
- { test: QA::Factory::Product::Attribute.new(:test, proc { 'returned' }) }
- end
-
- let(:product) { spy('product') }
-
- before do
- allow(QA::Factory::Base).to receive(:attributes).and_return(attributes)
- end
-
- describe '.populate!' do
- it 'returns a fabrication product and define factory attributes as its methods' do
- expect(described_class).to receive(:new).and_return(product)
-
- result = described_class.populate!(factory) do |instance|
- instance.something = 'string'
- end
-
- expect(result).to be product
- expect(result.test).to eq('returned')
- end
- end
-
- describe '.visit!' do
- it 'makes it possible to visit fabrication product' do
- allow_any_instance_of(described_class)
- .to receive(:current_url).and_return('some url')
- allow_any_instance_of(described_class)
- .to receive(:visit).and_return('visited some url')
-
- expect(subject.visit!).to eq 'visited some url'
- end
- end
-end
diff --git a/qa/spec/factory/resource/user_spec.rb b/qa/spec/factory/resource/user_spec.rb
new file mode 100644
index 00000000000..820c506b715
--- /dev/null
+++ b/qa/spec/factory/resource/user_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+describe QA::Resource::User do
+ describe "#fabricate_via_api!" do
+ Response = Struct.new(:code, :body)
+
+ it 'fetches an existing user' do
+ existing_users = [
+ {
+ id: '0',
+ name: 'name',
+ username: 'name',
+ web_url: ''
+ }
+ ]
+ users_response = Response.new('200', JSON.dump(existing_users))
+ single_user_response = Response.new('200', JSON.dump(existing_users.first))
+
+ expect(subject).to receive(:api_get_from).with("/users?username=name").and_return(users_response)
+ expect(subject).to receive(:api_get_from).with("/users/0").and_return(single_user_response)
+
+ subject.username = 'name'
+ subject.fabricate_via_api!
+
+ expect(subject.api_response).to eq(existing_users.first)
+ end
+
+ it 'tries to create a user if it does not exist' do
+ expect(subject).to receive(:api_get_from).with("/users?username=foo").and_return(Response.new('200', '[]'))
+ expect(subject).to receive(:api_post).and_return({ web_url: '' })
+
+ subject.username = 'foo'
+ subject.fabricate_via_api!
+ end
+ end
+end
diff --git a/qa/spec/git/repository_spec.rb b/qa/spec/git/repository_spec.rb
index 53bff3bf0b3..faa154c78da 100644
--- a/qa/spec/git/repository_spec.rb
+++ b/qa/spec/git/repository_spec.rb
@@ -1,17 +1,18 @@
describe QA::Git::Repository do
+ include Support::StubENV
+
let(:repository) { described_class.new }
before do
+ stub_env('GITLAB_USERNAME', 'root')
cd_empty_temp_directory
set_bad_uri
repository.use_default_credentials
end
describe '#clone' do
- it 'redacts credentials from the URI in output' do
- output, _ = repository.clone
-
- expect(output).to include("fatal: unable to access 'http://****@foo/bar.git/'")
+ it 'is unable to resolve host' do
+ expect(repository.clone).to include("fatal: unable to access 'http://root@foo/bar.git/'")
end
end
@@ -20,10 +21,38 @@ describe QA::Git::Repository do
`git init` # need a repo to push from
end
- it 'redacts credentials from the URI in output' do
- output, _ = repository.push_changes
+ it 'fails to push changes' do
+ expect(repository.push_changes).to include("error: failed to push some refs to 'http://root@foo/bar.git'")
+ end
+ end
+
+ describe '#git_protocol=' do
+ [0, 1, 2].each do |version|
+ it "configures git to use protocol version #{version}" do
+ expect(repository).to receive(:run).with("git config protocol.version #{version}")
+ repository.git_protocol = version
+ end
+ end
+
+ it 'raises an error if the version is unsupported' do
+ expect { repository.git_protocol = 'foo' }.to raise_error(ArgumentError, "Please specify the protocol you would like to use: 0, 1, or 2")
+ end
+ end
+
+ describe '#fetch_supported_git_protocol' do
+ it "reports the detected version" do
+ expect(repository).to receive(:run).and_return("packet: git< version 2")
+ expect(repository.fetch_supported_git_protocol).to eq('2')
+ end
+
+ it 'reports unknown if version is unknown' do
+ expect(repository).to receive(:run).and_return("packet: git< version -1")
+ expect(repository.fetch_supported_git_protocol).to eq('unknown')
+ end
- expect(output).to include("error: failed to push some refs to 'http://****@foo/bar.git'")
+ it 'reports unknown if content does not identify a version' do
+ expect(repository).to receive(:run).and_return("foo")
+ expect(repository.fetch_supported_git_protocol).to eq('unknown')
end
end
diff --git a/qa/spec/page/base_spec.rb b/qa/spec/page/base_spec.rb
index 52daa9697ee..076a8087db5 100644
--- a/qa/spec/page/base_spec.rb
+++ b/qa/spec/page/base_spec.rb
@@ -9,12 +9,12 @@ describe QA::Page::Base do
subject do
Class.new(described_class) do
view 'path/to/some/view.html.haml' do
- element :something, 'string pattern'
- element :something_else, /regexp pattern/
+ element :something, 'string pattern' # rubocop:disable QA/ElementWithPattern
+ element :something_else, /regexp pattern/ # rubocop:disable QA/ElementWithPattern
end
view 'path/to/some/_partial.html.haml' do
- element :another_element, 'string pattern'
+ element :another_element, 'string pattern' # rubocop:disable QA/ElementWithPattern
end
end
end
diff --git a/qa/spec/page/logging_spec.rb b/qa/spec/page/logging_spec.rb
new file mode 100644
index 00000000000..9d56353062b
--- /dev/null
+++ b/qa/spec/page/logging_spec.rb
@@ -0,0 +1,94 @@
+# frozen_string_literal: true
+
+require 'capybara/dsl'
+
+describe QA::Support::Page::Logging do
+ include Support::StubENV
+
+ let(:page) { double().as_null_object }
+
+ before do
+ logger = Logger.new $stdout
+ logger.level = ::Logger::DEBUG
+ QA::Runtime::Logger.logger = logger
+
+ allow(Capybara).to receive(:current_session).and_return(page)
+ allow(page).to receive(:current_url).and_return('http://current-url')
+ allow(page).to receive(:has_css?).with(any_args).and_return(true)
+ end
+
+ subject do
+ Class.new(QA::Page::Base) do
+ prepend QA::Support::Page::Logging
+ end.new
+ end
+
+ it 'logs refresh' do
+ expect { subject.refresh }
+ .to output(%r{refreshing http://current-url}).to_stdout_from_any_process
+ end
+
+ it 'logs wait' do
+ 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
+ end
+
+ it 'logs scroll_to' do
+ expect { subject.scroll_to(:element) }
+ .to output(/scrolling to :element/).to_stdout_from_any_process
+ end
+
+ it 'logs asset_exists?' do
+ expect { subject.asset_exists?('http://asset-url') }
+ .to output(%r{asset_exists\? http://asset-url returned false}).to_stdout_from_any_process
+ end
+
+ it 'logs find_element' do
+ expect { subject.find_element(:element) }
+ .to output(/found :element/).to_stdout_from_any_process
+ end
+
+ it 'logs click_element' do
+ expect { subject.click_element(:element) }
+ .to output(/clicking :element/).to_stdout_from_any_process
+ end
+
+ it 'logs fill_element' do
+ expect { subject.fill_element(:element, 'foo') }
+ .to output(/filling :element with "foo"/).to_stdout_from_any_process
+ end
+
+ it 'logs has_element?' do
+ expect { subject.has_element?(:element) }
+ .to output(/has_element\? :element returned true/).to_stdout_from_any_process
+ end
+
+ it 'logs within_element' do
+ expect { subject.within_element(:element) }
+ .to output(/within element :element/).to_stdout_from_any_process
+ expect { subject.within_element(:element) }
+ .to output(/end within element :element/).to_stdout_from_any_process
+ end
+
+ context 'all_elements' do
+ it 'logs the number of elements found' do
+ allow(page).to receive(:all).and_return([1, 2])
+
+ expect { subject.all_elements(:element) }
+ .to output(/finding all :element/).to_stdout_from_any_process
+ expect { subject.all_elements(:element) }
+ .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) }
+ .to output(/finding all :element/).to_stdout_from_any_process
+ expect { subject.all_elements(:element) }
+ .not_to output(/found 0 :elements/).to_stdout_from_any_process
+ end
+ end
+end
diff --git a/qa/spec/page/validator_spec.rb b/qa/spec/page/validator_spec.rb
index 55957649904..0ae6e66d767 100644
--- a/qa/spec/page/validator_spec.rb
+++ b/qa/spec/page/validator_spec.rb
@@ -30,7 +30,7 @@ describe QA::Page::Validator do
let(:view) { spy('view') }
before do
- allow(QA::Page::Admin::Settings::Main)
+ allow(QA::Page::Admin::Settings::Repository)
.to receive(:views).and_return([view])
end
diff --git a/qa/spec/page/view_spec.rb b/qa/spec/page/view_spec.rb
index 34d2ff11447..d7b3ccd316d 100644
--- a/qa/spec/page/view_spec.rb
+++ b/qa/spec/page/view_spec.rb
@@ -8,8 +8,8 @@ describe QA::Page::View do
describe '.evaluate' do
it 'evaluates a block and returns a DSL object' do
results = described_class.evaluate do
- element :something, 'my pattern'
- element :something_else, /another pattern/
+ element :something
+ element :something_else
end
expect(results.elements.size).to eq 2
diff --git a/qa/spec/resource/api_fabricator_spec.rb b/qa/spec/resource/api_fabricator_spec.rb
new file mode 100644
index 00000000000..a5ed4422f6e
--- /dev/null
+++ b/qa/spec/resource/api_fabricator_spec.rb
@@ -0,0 +1,161 @@
+# frozen_string_literal: true
+
+describe QA::Resource::ApiFabricator do
+ let(:resource_without_api_support) do
+ Class.new do
+ def self.name
+ 'FooBarResource'
+ end
+ end
+ end
+
+ let(:resource_with_api_support) do
+ Class.new do
+ def self.name
+ 'FooBarResource'
+ end
+
+ def api_get_path
+ '/foo'
+ end
+
+ def api_post_path
+ '/bar'
+ end
+
+ def api_post_body
+ { name: 'John Doe' }
+ end
+ end
+ end
+
+ before do
+ allow(subject).to receive(:current_url).and_return('')
+ end
+
+ subject { resource.tap { |f| f.include(described_class) }.new }
+
+ describe '#api_support?' do
+ let(:api_client) { spy('Runtime::API::Client') }
+ let(:api_client_instance) { double('API Client') }
+
+ context 'when resource does not support fabrication via the API' do
+ let(:resource) { resource_without_api_support }
+
+ it 'returns false' do
+ expect(subject).not_to be_api_support
+ end
+ end
+
+ context 'when resource supports fabrication via the API' do
+ let(:resource) { resource_with_api_support }
+
+ it 'returns false' do
+ expect(subject).to be_api_support
+ end
+ end
+ end
+
+ describe '#fabricate_via_api!' do
+ let(:api_client) { spy('Runtime::API::Client') }
+ let(:api_client_instance) { double('API Client') }
+
+ before do
+ stub_const('QA::Runtime::API::Client', api_client)
+
+ allow(api_client).to receive(:new).and_return(api_client_instance)
+ allow(api_client_instance).to receive(:personal_access_token).and_return('foo')
+ end
+
+ context 'when resource does not support fabrication via the API' do
+ let(:resource) { resource_without_api_support }
+
+ it 'raises a NotImplementedError exception' do
+ expect { subject.fabricate_via_api! }.to raise_error(NotImplementedError, "Resource FooBarResource does not support fabrication via the API!")
+ end
+ end
+
+ context 'when resource supports fabrication via the API' do
+ let(:resource) { resource_with_api_support }
+ let(:api_request) { spy('Runtime::API::Request') }
+ let(:resource_web_url) { 'http://example.org/api/v4/foo' }
+ let(:response) { { id: 1, name: 'John Doe', web_url: resource_web_url } }
+ let(:raw_post) { double('Raw POST response', code: 201, body: response.to_json) }
+
+ before do
+ stub_const('QA::Runtime::API::Request', api_request)
+
+ allow(api_request).to receive(:new).and_return(double(url: resource_web_url))
+ end
+
+ context 'when creating a resource' do
+ before do
+ allow(subject).to receive(:post).with(resource_web_url, subject.api_post_body).and_return(raw_post)
+ end
+
+ it 'returns the resource URL' do
+ expect(api_request).to receive(:new).with(api_client_instance, subject.api_post_path).and_return(double(url: resource_web_url))
+ expect(subject).to receive(:post).with(resource_web_url, subject.api_post_body).and_return(raw_post)
+
+ expect(subject.fabricate_via_api!).to eq(resource_web_url)
+ end
+
+ it 'populates api_resource with the resource' do
+ subject.fabricate_via_api!
+
+ expect(subject.api_resource).to eq(response)
+ end
+
+ context 'when the POST fails' do
+ let(:post_response) { { error: "Name already taken." } }
+ let(:raw_post) { double('Raw POST response', code: 400, body: post_response.to_json) }
+
+ it 'raises a ResourceFabricationFailedError exception' do
+ expect(api_request).to receive(:new).with(api_client_instance, subject.api_post_path).and_return(double(url: resource_web_url))
+ expect(subject).to receive(:post).with(resource_web_url, subject.api_post_body).and_return(raw_post)
+
+ expect { subject.fabricate_via_api! }.to raise_error(described_class::ResourceFabricationFailedError, "Fabrication of FooBarResource using the API failed (400) with `#{raw_post}`.")
+ expect(subject.api_resource).to be_nil
+ end
+ end
+ end
+
+ context '#transform_api_resource' do
+ let(:resource) do
+ Class.new do
+ def self.name
+ 'FooBarResource'
+ end
+
+ def api_get_path
+ '/foo'
+ end
+
+ def api_post_path
+ '/bar'
+ end
+
+ def api_post_body
+ { name: 'John Doe' }
+ end
+
+ def transform_api_resource(resource)
+ resource[:new] = 'foobar'
+ resource
+ end
+ end
+ end
+
+ let(:response) { { existing: 'foo', web_url: resource_web_url } }
+ let(:transformed_resource) { { existing: 'foo', new: 'foobar', web_url: resource_web_url } }
+
+ it 'transforms the resource' do
+ expect(subject).to receive(:post).with(resource_web_url, subject.api_post_body).and_return(raw_post)
+ expect(subject).to receive(:transform_api_resource).with(response).and_return(transformed_resource)
+
+ subject.fabricate_via_api!
+ end
+ end
+ end
+ end
+end
diff --git a/qa/spec/resource/base_spec.rb b/qa/spec/resource/base_spec.rb
new file mode 100644
index 00000000000..dc9e16792d3
--- /dev/null
+++ b/qa/spec/resource/base_spec.rb
@@ -0,0 +1,246 @@
+# frozen_string_literal: true
+
+describe QA::Resource::Base do
+ include Support::StubENV
+
+ let(:resource) { spy('resource') }
+ let(:location) { 'http://location' }
+
+ shared_context 'fabrication context' do
+ subject do
+ Class.new(described_class) do
+ def self.name
+ 'MyResource'
+ end
+ end
+ end
+
+ before do
+ allow(subject).to receive(:current_url).and_return(location)
+ allow(subject).to receive(:new).and_return(resource)
+ end
+ end
+
+ shared_examples 'fabrication method' do |fabrication_method_called, actual_fabrication_method = nil|
+ let(:fabrication_method_used) { actual_fabrication_method || fabrication_method_called }
+
+ it 'yields resource before calling resource method' do
+ expect(resource).to receive(:something!).ordered
+ expect(resource).to receive(fabrication_method_used).ordered.and_return(location)
+
+ subject.public_send(fabrication_method_called, resource: resource) do |resource|
+ resource.something!
+ end
+ end
+
+ it 'does not log the resource and build method when QA_DEBUG=false' do
+ stub_env('QA_DEBUG', 'false')
+ expect(resource).to receive(fabrication_method_used).and_return(location)
+
+ expect { subject.public_send(fabrication_method_called, 'something', resource: resource) }
+ .not_to output.to_stdout
+ end
+ end
+
+ describe '.fabricate!' do
+ context 'when resource does not support fabrication via the API' do
+ before do
+ expect(described_class).to receive(:fabricate_via_api!).and_raise(NotImplementedError)
+ end
+
+ it 'calls .fabricate_via_browser_ui!' do
+ expect(described_class).to receive(:fabricate_via_browser_ui!)
+
+ described_class.fabricate!
+ end
+ end
+
+ context 'when resource supports fabrication via the API' do
+ it 'calls .fabricate_via_browser_ui!' do
+ expect(described_class).to receive(:fabricate_via_api!)
+
+ described_class.fabricate!
+ end
+ end
+ end
+
+ describe '.fabricate_via_api!' do
+ include_context 'fabrication context'
+
+ it_behaves_like 'fabrication method', :fabricate_via_api!
+
+ it 'instantiates the resource, calls resource method returns the resource' do
+ expect(resource).to receive(:fabricate_via_api!).and_return(location)
+
+ result = subject.fabricate_via_api!(resource: resource, parents: [])
+
+ expect(result).to eq(resource)
+ end
+
+ it 'logs the resource and build method when QA_DEBUG=true' do
+ stub_env('QA_DEBUG', 'true')
+ expect(resource).to receive(:fabricate_via_api!).and_return(location)
+
+ expect { subject.fabricate_via_api!('something', resource: resource, parents: []) }
+ .to output(/==> Built a MyResource via api in [\d\.\-e]+ seconds+/)
+ .to_stdout
+ end
+ end
+
+ describe '.fabricate_via_browser_ui!' do
+ include_context 'fabrication context'
+
+ it_behaves_like 'fabrication method', :fabricate_via_browser_ui!, :fabricate!
+
+ it 'instantiates the resource and calls resource method' do
+ subject.fabricate_via_browser_ui!('something', resource: resource, parents: [])
+
+ expect(resource).to have_received(:fabricate!).with('something')
+ end
+
+ it 'returns fabrication resource' do
+ result = subject.fabricate_via_browser_ui!('something', resource: resource, parents: [])
+
+ expect(result).to eq(resource)
+ end
+
+ it 'logs the resource and build method when QA_DEBUG=true' do
+ stub_env('QA_DEBUG', 'true')
+
+ expect { subject.fabricate_via_browser_ui!('something', resource: resource, parents: []) }
+ .to output(/==> Built a MyResource via browser_ui in [\d\.\-e]+ seconds+/)
+ .to_stdout
+ end
+ end
+
+ shared_context 'simple resource' do
+ subject do
+ Class.new(QA::Resource::Base) do
+ attribute :test do
+ 'block'
+ end
+
+ attribute :no_block
+
+ def fabricate!
+ 'any'
+ end
+
+ def self.current_url
+ 'http://stub'
+ end
+ end
+ end
+
+ let(:resource) { subject.new }
+ end
+
+ describe '.attribute' do
+ include_context 'simple resource'
+
+ it 'appends new attribute' do
+ expect(subject.attributes_names).to eq([:no_block, :test, :web_url])
+ end
+
+ context 'when the attribute is populated via a block' do
+ it 'returns value from the block' do
+ result = subject.fabricate!(resource: resource)
+
+ expect(result).to be_a(described_class)
+ expect(result.test).to eq('block')
+ end
+ end
+
+ context 'when the attribute is populated via the api' do
+ let(:api_resource) { { no_block: 'api' } }
+
+ before do
+ expect(resource).to receive(:api_resource).and_return(api_resource)
+ end
+
+ it 'returns value from api' do
+ result = subject.fabricate!(resource: resource)
+
+ expect(result).to be_a(described_class)
+ expect(result.no_block).to eq('api')
+ end
+
+ context 'when the attribute also has a block' do
+ let(:api_resource) { { test: 'api_with_block' } }
+
+ before do
+ allow(QA::Runtime::Logger).to receive(:info)
+ end
+
+ it 'returns value from api and emits an INFO log entry' do
+ result = subject.fabricate!(resource: resource)
+
+ expect(result).to be_a(described_class)
+ expect(result.test).to eq('api_with_block')
+ expect(QA::Runtime::Logger)
+ .to have_received(:info).with(/api_with_block/)
+ end
+ end
+ end
+
+ context 'when the attribute is populated via direct assignment' do
+ before do
+ resource.test = 'value'
+ end
+
+ it 'returns value from the assignment' do
+ result = subject.fabricate!(resource: resource)
+
+ expect(result).to be_a(described_class)
+ expect(result.test).to eq('value')
+ end
+
+ context 'when the api also has such response' do
+ before do
+ allow(resource).to receive(:api_resource).and_return({ test: 'api' })
+ end
+
+ it 'returns value from the assignment' do
+ result = subject.fabricate!(resource: resource)
+
+ expect(result).to be_a(described_class)
+ expect(result.test).to eq('value')
+ end
+ end
+ end
+
+ context 'when the attribute has no value' do
+ it 'raises an error because no values could be found' do
+ result = subject.fabricate!(resource: resource)
+
+ expect { result.no_block }
+ .to raise_error(described_class::NoValueError, "No value was computed for no_block of #{resource.class.name}.")
+ end
+ end
+ end
+
+ describe '#web_url' do
+ include_context 'simple resource'
+
+ it 'sets #web_url to #current_url after fabrication' do
+ subject.fabricate!(resource: resource)
+
+ expect(resource.web_url).to eq(subject.current_url)
+ end
+ end
+
+ describe '#visit!' do
+ include_context 'simple resource'
+
+ before do
+ allow(resource).to receive(:visit)
+ end
+
+ it 'calls #visit with the underlying #web_url' do
+ resource.web_url = subject.current_url
+ resource.visit!
+
+ expect(resource).to have_received(:visit).with(subject.current_url)
+ end
+ end
+end
diff --git a/qa/spec/resource/repository/push_spec.rb b/qa/spec/resource/repository/push_spec.rb
new file mode 100644
index 00000000000..bf3ebce0cfe
--- /dev/null
+++ b/qa/spec/resource/repository/push_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+describe QA::Resource::Repository::Push do
+ describe '.files=' do
+ let(:files) do
+ [
+ {
+ name: 'file.txt',
+ content: 'foo'
+ }
+ ]
+ end
+
+ it 'raises an error if files is not an array' do
+ expect { subject.files = '' }.to raise_error(ArgumentError)
+ end
+
+ it 'raises an error if files is an empty array' do
+ expect { subject.files = [] }.to raise_error(ArgumentError)
+ end
+
+ it 'does not raise if files is an array' do
+ expect { subject.files = files }.not_to raise_error
+ end
+ end
+end
diff --git a/qa/spec/runtime/api/client_spec.rb b/qa/spec/runtime/api/client_spec.rb
index d497d8839b8..975586b505f 100644
--- a/qa/spec/runtime/api/client_spec.rb
+++ b/qa/spec/runtime/api/client_spec.rb
@@ -13,18 +13,27 @@ describe QA::Runtime::API::Client do
end
end
- describe '#get_personal_access_token' do
- it 'returns specified token from env' do
- stub_env('PERSONAL_ACCESS_TOKEN', 'a_token')
+ describe '#personal_access_token' do
+ context 'when QA::Runtime::Env.personal_access_token is present' do
+ before do
+ allow(QA::Runtime::Env).to receive(:personal_access_token).and_return('a_token')
+ end
- expect(described_class.new.get_personal_access_token).to eq 'a_token'
+ it 'returns specified token from env' do
+ expect(described_class.new.personal_access_token).to eq 'a_token'
+ end
end
- it 'returns a created token' do
- allow_any_instance_of(described_class)
- .to receive(:create_personal_access_token).and_return('created_token')
+ context 'when QA::Runtime::Env.personal_access_token is nil' do
+ before do
+ allow(QA::Runtime::Env).to receive(:personal_access_token).and_return(nil)
+ end
- expect(described_class.new.get_personal_access_token).to eq 'created_token'
+ it 'returns a created token' do
+ expect(subject).to receive(:create_personal_access_token).and_return('created_token')
+
+ expect(subject.personal_access_token).to eq 'created_token'
+ end
end
end
end
diff --git a/qa/spec/runtime/api/request_spec.rb b/qa/spec/runtime/api/request_spec.rb
index 80e3149f32d..08233e3c1d6 100644
--- a/qa/spec/runtime/api/request_spec.rb
+++ b/qa/spec/runtime/api/request_spec.rb
@@ -1,17 +1,23 @@
describe QA::Runtime::API::Request do
- include Support::StubENV
+ let(:client) { QA::Runtime::API::Client.new('http://example.com') }
+ let(:request) { described_class.new(client, '/users') }
before do
- stub_env('PERSONAL_ACCESS_TOKEN', 'a_token')
+ allow(client).to receive(:personal_access_token).and_return('a_token')
end
- let(:client) { QA::Runtime::API::Client.new('http://example.com') }
- let(:request) { described_class.new(client, '/users') }
-
describe '#url' do
- it 'returns the full api request url' do
+ it 'returns the full API request url' do
expect(request.url).to eq 'http://example.com/api/v4/users?private_token=a_token'
end
+
+ context 'when oauth_access_token is passed in the query string' do
+ let(:request) { described_class.new(client, '/users', { oauth_access_token: 'foo' }) }
+
+ it 'does not adds a private_token query string' do
+ expect(request.url).to eq 'http://example.com/api/v4/users?oauth_access_token=foo'
+ end
+ end
end
describe '#request_path' do
diff --git a/qa/spec/runtime/api_request_spec.rb b/qa/spec/runtime/api_request_spec.rb
deleted file mode 100644
index e69de29bb2d..00000000000
--- a/qa/spec/runtime/api_request_spec.rb
+++ /dev/null
diff --git a/qa/spec/runtime/env_spec.rb b/qa/spec/runtime/env_spec.rb
index d889d185a45..ded51d5bb7c 100644
--- a/qa/spec/runtime/env_spec.rb
+++ b/qa/spec/runtime/env_spec.rb
@@ -1,39 +1,66 @@
+# frozen_string_literal: true
+
describe QA::Runtime::Env do
include Support::StubENV
- describe '.chrome_headless?' do
+ shared_examples 'boolean method' do |**kwargs|
+ it_behaves_like 'boolean method with parameter', kwargs
+ end
+
+ shared_examples 'boolean method with parameter' do |method:, param: nil, env_key:, default:|
context 'when there is an env variable set' do
it 'returns false when falsey values specified' do
- stub_env('CHROME_HEADLESS', 'false')
- expect(described_class.chrome_headless?).to be_falsey
+ stub_env(env_key, 'false')
+ expect(described_class.public_send(method, *param)).to be_falsey
- stub_env('CHROME_HEADLESS', 'no')
- expect(described_class.chrome_headless?).to be_falsey
+ stub_env(env_key, 'no')
+ expect(described_class.public_send(method, *param)).to be_falsey
- stub_env('CHROME_HEADLESS', '0')
- expect(described_class.chrome_headless?).to be_falsey
+ stub_env(env_key, '0')
+ expect(described_class.public_send(method, *param)).to be_falsey
end
it 'returns true when anything else specified' do
- stub_env('CHROME_HEADLESS', 'true')
- expect(described_class.chrome_headless?).to be_truthy
+ stub_env(env_key, 'true')
+ expect(described_class.public_send(method, *param)).to be_truthy
- stub_env('CHROME_HEADLESS', '1')
- expect(described_class.chrome_headless?).to be_truthy
+ stub_env(env_key, '1')
+ expect(described_class.public_send(method, *param)).to be_truthy
- stub_env('CHROME_HEADLESS', 'anything')
- expect(described_class.chrome_headless?).to be_truthy
+ stub_env(env_key, 'anything')
+ expect(described_class.public_send(method, *param)).to be_truthy
end
end
context 'when there is no env variable set' do
- it 'returns the default, true' do
- stub_env('CHROME_HEADLESS', nil)
- expect(described_class.chrome_headless?).to be_truthy
+ it "returns the default, #{default}" do
+ stub_env(env_key, nil)
+ expect(described_class.public_send(method, *param)).to be(default)
end
end
end
+ describe '.signup_disabled?' do
+ it_behaves_like 'boolean method',
+ method: :signup_disabled?,
+ env_key: 'SIGNUP_DISABLED',
+ default: false
+ end
+
+ describe '.debug?' do
+ it_behaves_like 'boolean method',
+ method: :debug?,
+ env_key: 'QA_DEBUG',
+ default: false
+ end
+
+ describe '.chrome_headless?' do
+ it_behaves_like 'boolean method',
+ method: :chrome_headless?,
+ env_key: 'CHROME_HEADLESS',
+ default: true
+ end
+
describe '.running_in_ci?' do
context 'when there is an env variable set' do
it 'returns true if CI' do
@@ -56,7 +83,54 @@ describe QA::Runtime::Env do
end
end
+ describe '.personal_access_token' do
+ around do |example|
+ described_class.instance_variable_set(:@personal_access_token, nil)
+ example.run
+ described_class.instance_variable_set(:@personal_access_token, nil)
+ end
+
+ context 'when PERSONAL_ACCESS_TOKEN is set' do
+ before do
+ stub_env('PERSONAL_ACCESS_TOKEN', 'a_token')
+ end
+
+ it 'returns specified token from env' do
+ expect(described_class.personal_access_token).to eq 'a_token'
+ end
+ end
+
+ context 'when @personal_access_token is set' do
+ before do
+ described_class.personal_access_token = 'another_token'
+ end
+
+ it 'returns the instance variable value' do
+ expect(described_class.personal_access_token).to eq 'another_token'
+ end
+ end
+ end
+
+ describe '.personal_access_token=' do
+ around do |example|
+ described_class.instance_variable_set(:@personal_access_token, nil)
+ example.run
+ described_class.instance_variable_set(:@personal_access_token, nil)
+ end
+
+ it 'saves the token' do
+ described_class.personal_access_token = 'a_token'
+
+ expect(described_class.personal_access_token).to eq 'a_token'
+ end
+ end
+
describe '.forker?' do
+ before do
+ stub_env('GITLAB_FORKER_USERNAME', nil)
+ stub_env('GITLAB_FORKER_PASSWORD', nil)
+ end
+
it 'returns false if no forker credentials are defined' do
expect(described_class).not_to be_forker
end
@@ -107,4 +181,30 @@ describe QA::Runtime::Env do
expect { described_class.require_github_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)
+
+ expect(described_class.log_destination).to eq($stdout)
+ end
+
+ it 'returns the path if QA_LOG_PATH is defined' do
+ stub_env('QA_LOG_PATH', 'path/to_file')
+
+ expect(described_class.log_destination).to eq('path/to_file')
+ end
+ end
+
+ describe '.can_test?' do
+ it_behaves_like 'boolean method with parameter',
+ method: :can_test?,
+ param: :git_protocol_v2,
+ env_key: 'QA_CAN_TEST_GIT_PROTOCOL_V2',
+ default: true
+
+ it 'raises ArgumentError if feature is unknown' do
+ expect { described_class.can_test? :foo }.to raise_error(ArgumentError, 'Unknown feature "foo"')
+ end
+ end
end
diff --git a/qa/spec/runtime/logger_spec.rb b/qa/spec/runtime/logger_spec.rb
new file mode 100644
index 00000000000..44be3381bff
--- /dev/null
+++ b/qa/spec/runtime/logger_spec.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+describe QA::Runtime::Logger do
+ before do
+ logger = Logger.new $stdout
+ logger.level = ::Logger::DEBUG
+ described_class.logger = logger
+ end
+
+ it 'logs debug' do
+ expect { described_class.debug('test') }.to output(/DEBUG -- : test/).to_stdout_from_any_process
+ end
+
+ it 'logs info' do
+ expect { described_class.info('test') }.to output(/INFO -- : test/).to_stdout_from_any_process
+ end
+
+ it 'logs warn' do
+ expect { described_class.warn('test') }.to output(/WARN -- : test/).to_stdout_from_any_process
+ end
+
+ it 'logs error' do
+ expect { described_class.error('test') }.to output(/ERROR -- : test/).to_stdout_from_any_process
+ end
+
+ it 'logs fatal' do
+ expect { described_class.fatal('test') }.to output(/FATAL -- : test/).to_stdout_from_any_process
+ end
+
+ it 'logs unknown' do
+ expect { described_class.unknown('test') }.to output(/ANY -- : test/).to_stdout_from_any_process
+ end
+end
diff --git a/qa/spec/scenario/test/integration/instance_saml_spec.rb b/qa/spec/scenario/test/integration/instance_saml_spec.rb
new file mode 100644
index 00000000000..cb8a6a630cc
--- /dev/null
+++ b/qa/spec/scenario/test/integration/instance_saml_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+describe QA::Scenario::Test::Integration::InstanceSAML do
+ context '#perform' do
+ it_behaves_like 'a QA scenario class' do
+ let(:tags) { [:instance_saml] }
+ end
+ end
+end
diff --git a/qa/spec/scenario/test/integration/ldap_spec.rb b/qa/spec/scenario/test/integration/ldap_spec.rb
index 198856aec3f..b6d798bf504 100644
--- a/qa/spec/scenario/test/integration/ldap_spec.rb
+++ b/qa/spec/scenario/test/integration/ldap_spec.rb
@@ -1,9 +1,17 @@
# frozen_string_literal: true
-describe QA::Scenario::Test::Integration::LDAP do
+describe QA::Scenario::Test::Integration::LDAPNoTLS do
context '#perform' do
it_behaves_like 'a QA scenario class' do
- let(:tags) { [:ldap] }
+ let(:tags) { [:ldap_no_tls] }
+ end
+ end
+end
+
+describe QA::Scenario::Test::Integration::LDAPTLS do
+ context '#perform' do
+ it_behaves_like 'a QA scenario class' do
+ let(:tags) { [:ldap_tls] }
end
end
end
diff --git a/qa/spec/spec_helper.rb b/qa/spec/spec_helper.rb
index 8e6613cd688..8e01da01340 100644
--- a/qa/spec/spec_helper.rb
+++ b/qa/spec/spec_helper.rb
@@ -3,6 +3,10 @@ require_relative '../qa'
Dir[::File.join(__dir__, 'support', '**', '*.rb')].each { |f| require f }
RSpec.configure do |config|
+ config.before do |example|
+ QA::Runtime::Logger.debug("Starting test: #{example.full_description}") if QA::Runtime::Env.debug?
+ end
+
config.expect_with :rspec do |expectations|
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
end
diff --git a/qa/spec/specs/runner_spec.rb b/qa/spec/specs/runner_spec.rb
index cf22d1c9395..741821ddf8c 100644
--- a/qa/spec/specs/runner_spec.rb
+++ b/qa/spec/specs/runner_spec.rb
@@ -62,6 +62,34 @@ describe QA::Specs::Runner do
end
end
+ context 'when SIGNUP_DISABLED is true' do
+ before do
+ allow(QA::Runtime::Env).to receive(:signup_disabled?).and_return(true)
+ end
+
+ subject { described_class.new }
+
+ it 'it includes default args and excludes the skip_signup_disabled tag' do
+ expect_rspec_runner_arguments(['--tag', '~orchestrated', '--tag', '~skip_signup_disabled', *described_class::DEFAULT_TEST_PATH_ARGS])
+
+ subject.perform
+ end
+ end
+
+ context 'when git protocol v2 is not supported' do
+ before do
+ allow(QA::Runtime::Env).to receive(:can_test?).with(:git_protocol_v2).and_return(false)
+ end
+
+ subject { described_class.new }
+
+ it 'it includes default args and excludes the requires_git_protocol_v2 tag' do
+ expect_rspec_runner_arguments(['--tag', '~orchestrated', '--tag', '~requires_git_protocol_v2', *described_class::DEFAULT_TEST_PATH_ARGS])
+
+ subject.perform
+ end
+ end
+
def expect_rspec_runner_arguments(arguments)
expect(RSpec::Core::Runner).to receive(:run)
.with(arguments, $stderr, $stdout)