summaryrefslogtreecommitdiff
path: root/qa
diff options
context:
space:
mode:
Diffstat (limited to 'qa')
-rw-r--r--qa/Gemfile2
-rw-r--r--qa/Gemfile.lock40
-rw-r--r--qa/README.md22
-rw-r--r--qa/qa.rb59
-rw-r--r--qa/qa/factory/base.rb41
-rw-r--r--qa/qa/factory/dependency.rb7
-rw-r--r--qa/qa/factory/product.rb17
-rw-r--r--qa/qa/factory/resource/deploy_key.rb14
-rw-r--r--qa/qa/factory/resource/issue.rb32
-rw-r--r--qa/qa/factory/resource/merge_request.rb49
-rw-r--r--qa/qa/factory/resource/personal_access_token.rb27
-rw-r--r--qa/qa/factory/resource/project.rb4
-rw-r--r--qa/qa/factory/resource/runner.rb42
-rw-r--r--qa/qa/factory/resource/secret_variable.rb41
-rw-r--r--qa/qa/page/README.md122
-rw-r--r--qa/qa/page/admin/settings.rb8
-rw-r--r--qa/qa/page/base.rb84
-rw-r--r--qa/qa/page/component/dropzone.rb31
-rw-r--r--qa/qa/page/dashboard/groups.rb9
-rw-r--r--qa/qa/page/dashboard/projects.rb13
-rw-r--r--qa/qa/page/element.rb32
-rw-r--r--qa/qa/page/group/new.rb11
-rw-r--r--qa/qa/page/group/show.rb48
-rw-r--r--qa/qa/page/main/login.rb32
-rw-r--r--qa/qa/page/main/oauth.rb4
-rw-r--r--qa/qa/page/mattermost/login.rb7
-rw-r--r--qa/qa/page/mattermost/main.rb7
-rw-r--r--qa/qa/page/menu/admin.rb4
-rw-r--r--qa/qa/page/menu/main.rb44
-rw-r--r--qa/qa/page/menu/profile.rb27
-rw-r--r--qa/qa/page/menu/side.rb65
-rw-r--r--qa/qa/page/merge_request/new.rb31
-rw-r--r--qa/qa/page/profile/personal_access_tokens.rb33
-rw-r--r--qa/qa/page/project/activity.rb15
-rw-r--r--qa/qa/page/project/issue/index.rb17
-rw-r--r--qa/qa/page/project/issue/new.rb33
-rw-r--r--qa/qa/page/project/issue/show.rb40
-rw-r--r--qa/qa/page/project/new.rb13
-rw-r--r--qa/qa/page/project/pipeline/index.rb13
-rw-r--r--qa/qa/page/project/pipeline/show.rb35
-rw-r--r--qa/qa/page/project/settings/advanced.rb33
-rw-r--r--qa/qa/page/project/settings/ci_cd.rb28
-rw-r--r--qa/qa/page/project/settings/common.rb24
-rw-r--r--qa/qa/page/project/settings/deploy_keys.rb35
-rw-r--r--qa/qa/page/project/settings/main.rb21
-rw-r--r--qa/qa/page/project/settings/repository.rb6
-rw-r--r--qa/qa/page/project/settings/runners.rb35
-rw-r--r--qa/qa/page/project/settings/secret_variables.rb57
-rw-r--r--qa/qa/page/project/show.rb44
-rw-r--r--qa/qa/page/validator.rb52
-rw-r--r--qa/qa/page/view.rb55
-rw-r--r--qa/qa/runtime/address.rb20
-rw-r--r--qa/qa/runtime/api.rb82
-rw-r--r--qa/qa/runtime/browser.rb25
-rw-r--r--qa/qa/runtime/env.rb6
-rw-r--r--qa/qa/runtime/namespace.rb4
-rw-r--r--qa/qa/runtime/rsa_key.rb21
-rw-r--r--qa/qa/runtime/user.rb11
-rw-r--r--qa/qa/scenario/entrypoint.rb34
-rw-r--r--qa/qa/scenario/taggable.rb17
-rw-r--r--qa/qa/scenario/test/instance.rb22
-rw-r--r--qa/qa/scenario/test/integration/mattermost.rb2
-rw-r--r--qa/qa/scenario/test/sanity/selectors.rb54
-rw-r--r--qa/qa/service/omnibus.rb20
-rw-r--r--qa/qa/service/runner.rb41
-rw-r--r--qa/qa/service/shellout.rb23
-rw-r--r--qa/qa/shell/omnibus.rb39
-rw-r--r--qa/qa/specs/features/api/users_spec.rb42
-rw-r--r--qa/qa/specs/features/merge_request/create_spec.rb17
-rw-r--r--qa/qa/specs/features/project/activity_spec.rb20
-rw-r--r--qa/qa/specs/features/project/add_deploy_key_spec.rb20
-rw-r--r--qa/qa/specs/features/project/add_secret_variable_spec.rb19
-rw-r--r--qa/qa/specs/features/project/create_issue_spec.rb18
-rw-r--r--qa/qa/specs/features/project/create_spec.rb4
-rw-r--r--qa/qa/specs/features/project/pipelines_spec.rb102
-rw-r--r--qa/qa/specs/features/repository/push_spec.rb5
-rw-r--r--qa/spec/factory/base_spec.rb67
-rw-r--r--qa/spec/factory/dependency_spec.rb13
-rw-r--r--qa/spec/factory/product_spec.rb21
-rw-r--r--qa/spec/fixtures/banana_sample.gifbin0 -> 71759 bytes
-rw-r--r--qa/spec/page/base_spec.rb63
-rw-r--r--qa/spec/page/element_spec.rb51
-rw-r--r--qa/spec/page/validator_spec.rb79
-rw-r--r--qa/spec/page/view_spec.rb70
-rw-r--r--qa/spec/runtime/api_client_spec.rb30
-rw-r--r--qa/spec/runtime/api_request_spec.rb42
-rw-r--r--qa/spec/runtime/env_spec.rb8
-rw-r--r--qa/spec/runtime/rsa_key.rb9
-rw-r--r--qa/spec/scenario/test/instance_spec.rb (renamed from qa/spec/scenario/entrypoint_spec.rb)4
-rw-r--r--qa/spec/scenario/test/sanity/selectors_spec.rb40
-rw-r--r--qa/spec/spec_helper.rb2
-rw-r--r--qa/spec/support/stub_env.rb38
92 files changed, 2527 insertions, 243 deletions
diff --git a/qa/Gemfile b/qa/Gemfile
index 4c866a3f893..c3e61568f3d 100644
--- a/qa/Gemfile
+++ b/qa/Gemfile
@@ -6,3 +6,5 @@ gem 'capybara-screenshot', '~> 1.0.18'
gem 'rake', '~> 12.3.0'
gem 'rspec', '~> 3.7'
gem 'selenium-webdriver', '~> 3.8.0'
+gem 'net-ssh', require: false
+gem 'airborne', '~> 0.2.13'
diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock
index 88d5fe834a0..51d2e4d7a10 100644
--- a/qa/Gemfile.lock
+++ b/qa/Gemfile.lock
@@ -1,8 +1,19 @@
GEM
remote: https://rubygems.org/
specs:
+ activesupport (5.1.4)
+ concurrent-ruby (~> 1.0, >= 1.0.2)
+ i18n (~> 0.7)
+ minitest (~> 5.1)
+ tzinfo (~> 1.1)
addressable (2.5.2)
public_suffix (>= 2.0.2, < 4.0)
+ airborne (0.2.13)
+ activesupport
+ rack
+ rack-test (~> 0.6, >= 0.6.2)
+ rest-client (>= 1.7.3, < 3.0)
+ rspec (~> 3.1)
byebug (9.1.0)
capybara (2.16.1)
addressable
@@ -17,13 +28,26 @@ GEM
childprocess (0.8.0)
ffi (~> 1.0, >= 1.0.11)
coderay (1.1.2)
+ concurrent-ruby (1.0.5)
diff-lcs (1.3)
+ domain_name (0.5.20170404)
+ unf (>= 0.0.5, < 1.0.0)
ffi (1.9.18)
+ http-cookie (1.0.3)
+ domain_name (~> 0.5)
+ i18n (0.9.1)
+ concurrent-ruby (~> 1.0)
launchy (2.4.3)
addressable (~> 2.3)
method_source (0.9.0)
+ mime-types (3.1)
+ mime-types-data (~> 3.2015)
+ mime-types-data (3.2016.0521)
mini_mime (1.0.0)
mini_portile2 (2.3.0)
+ minitest (5.11.1)
+ net-ssh (4.1.0)
+ netrc (0.11.0)
nokogiri (1.8.1)
mini_portile2 (~> 2.3.0)
pry (0.11.3)
@@ -37,11 +61,15 @@ GEM
rack-test (0.8.2)
rack (>= 1.0, < 3)
rake (12.3.0)
+ rest-client (2.0.2)
+ http-cookie (>= 1.0.2, < 2.0)
+ mime-types (>= 1.16, < 4.0)
+ netrc (~> 0.8)
rspec (3.7.0)
rspec-core (~> 3.7.0)
rspec-expectations (~> 3.7.0)
rspec-mocks (~> 3.7.0)
- rspec-core (3.7.0)
+ rspec-core (3.7.1)
rspec-support (~> 3.7.0)
rspec-expectations (3.7.0)
diff-lcs (>= 1.2.0, < 2.0)
@@ -54,6 +82,12 @@ GEM
selenium-webdriver (3.8.0)
childprocess (~> 0.5)
rubyzip (~> 1.0)
+ thread_safe (0.3.6)
+ tzinfo (1.2.4)
+ thread_safe (~> 0.1)
+ unf (0.1.4)
+ unf_ext
+ unf_ext (0.0.7.4)
xpath (2.1.0)
nokogiri (~> 1.3)
@@ -61,12 +95,14 @@ PLATFORMS
ruby
DEPENDENCIES
+ airborne (~> 0.2.13)
capybara (~> 2.16.1)
capybara-screenshot (~> 1.0.18)
+ net-ssh
pry-byebug (~> 3.5.1)
rake (~> 12.3.0)
rspec (~> 3.7)
selenium-webdriver (~> 3.8.0)
BUNDLED WITH
- 1.16.0
+ 1.16.1
diff --git a/qa/README.md b/qa/README.md
index 7f2dd39ff63..b937dc4c7a0 100644
--- a/qa/README.md
+++ b/qa/README.md
@@ -17,23 +17,41 @@ against any existing instance.
1. Along with GitLab Docker Images we also build and publish GitLab QA images.
1. GitLab QA project uses these images to execute integration tests.
+## Validating GitLab views / partials / selectors in merge requests
+
+We recently added a new CI job that is going to be triggered for every push
+event in CE and EE projects. The job is called `qa:selectors` and it will
+verify coupling between page objects implemented as a part of GitLab QA
+and corresponding views / partials / selectors in CE / EE.
+
+Whenever `qa:selectors` job fails in your merge request, you are supposed to
+fix [page objects](qa/page/README.md). You should also trigger end-to-end tests
+using `package-qa` manual action, to test if everything works fine.
+
## How can I use it?
You can use GitLab QA to exercise tests on any live instance! For example, the
following call would login to a local [GDK] instance and run all specs in
`qa/specs/features`:
+First, `cd` into the `$gdk/gitlab/qa` directory.
+The `bin/qa` script expects you to be in the `qa` folder of the app.
+
```
bin/qa Test::Instance http://localhost:3000
```
+### Writing tests
+
+1. [Using page objects](qa/page/README.md)
+
### Running specific tests
You can also supply specific tests to run as another parameter. For example, to
-test the EE license specs, you can run:
+run the repository-related specs, you can execute:
```
-EE_LICENSE="<YOUR LICENSE KEY>" bin/qa Test::Instance http://localhost qa/specs/features/ee
+bin/qa Test::Instance http://localhost qa/specs/features/repository/
```
Since the arguments would be passed to `rspec`, you could use all `rspec`
diff --git a/qa/qa.rb b/qa/qa.rb
index 71b80a6adcb..8630e2a522c 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -11,6 +11,9 @@ module QA
autoload :Scenario, 'qa/runtime/scenario'
autoload :Browser, 'qa/runtime/browser'
autoload :Env, 'qa/runtime/env'
+ autoload :RSAKey, 'qa/runtime/rsa_key'
+ autoload :Address, 'qa/runtime/address'
+ autoload :API, 'qa/runtime/api'
end
##
@@ -24,8 +27,13 @@ module QA
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 :DeployKey, 'qa/factory/resource/deploy_key'
+ autoload :SecretVariable, 'qa/factory/resource/secret_variable'
+ autoload :Runner, 'qa/factory/resource/runner'
+ autoload :PersonalAccessToken, 'qa/factory/resource/personal_access_token'
end
module Repository
@@ -46,7 +54,7 @@ module QA
#
autoload :Bootable, 'qa/scenario/bootable'
autoload :Actable, 'qa/scenario/actable'
- autoload :Entrypoint, 'qa/scenario/entrypoint'
+ autoload :Taggable, 'qa/scenario/taggable'
autoload :Template, 'qa/scenario/template'
##
@@ -58,6 +66,10 @@ module QA
module Integration
autoload :Mattermost, 'qa/scenario/test/integration/mattermost'
end
+
+ module Sanity
+ autoload :Selectors, 'qa/scenario/test/sanity/selectors'
+ end
end
end
@@ -68,6 +80,9 @@ module QA
#
module Page
autoload :Base, 'qa/page/base'
+ autoload :View, 'qa/page/view'
+ autoload :Element, 'qa/page/element'
+ autoload :Validator, 'qa/page/validator'
module Main
autoload :Login, 'qa/page/main/login'
@@ -78,6 +93,7 @@ module QA
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
@@ -93,12 +109,37 @@ module QA
module Project
autoload :New, 'qa/page/project/new'
autoload :Show, 'qa/page/project/show'
+ autoload :Activity, 'qa/page/project/activity'
+
+ module Pipeline
+ autoload :Index, 'qa/page/project/pipeline/index'
+ autoload :Show, 'qa/page/project/pipeline/show'
+ end
module Settings
autoload :Common, 'qa/page/project/settings/common'
+ autoload :Advanced, 'qa/page/project/settings/advanced'
+ autoload :Main, 'qa/page/project/settings/main'
autoload :Repository, 'qa/page/project/settings/repository'
+ autoload :CICD, 'qa/page/project/settings/ci_cd'
autoload :DeployKeys, 'qa/page/project/settings/deploy_keys'
+ autoload :SecretVariables, 'qa/page/project/settings/secret_variables'
+ autoload :Runners, 'qa/page/project/settings/runners'
end
+
+ module Issue
+ autoload :New, 'qa/page/project/issue/new'
+ autoload :Show, 'qa/page/project/issue/show'
+ autoload :Index, 'qa/page/project/issue/index'
+ end
+ end
+
+ module Profile
+ autoload :PersonalAccessTokens, 'qa/page/profile/personal_access_tokens'
+ end
+
+ module MergeRequest
+ autoload :New, 'qa/page/merge_request/new'
end
module Admin
@@ -109,6 +150,13 @@ module QA
autoload :Main, 'qa/page/mattermost/main'
autoload :Login, 'qa/page/mattermost/login'
end
+
+ ##
+ # Classes describing components that are used by several pages.
+ #
+ module Component
+ autoload :Dropzone, 'qa/page/component/dropzone'
+ end
end
##
@@ -119,10 +167,13 @@ module QA
end
##
- # Classes describing shell interaction with GitLab
+ # Classes describing services being part of GitLab and how we can interact
+ # with these services, like through the shell.
#
- module Shell
- autoload :Omnibus, 'qa/shell/omnibus'
+ module Service
+ autoload :Shellout, 'qa/service/shellout'
+ autoload :Omnibus, 'qa/service/omnibus'
+ autoload :Runner, 'qa/service/runner'
end
##
diff --git a/qa/qa/factory/base.rb b/qa/qa/factory/base.rb
index 00851a7bece..bd66b74a164 100644
--- a/qa/qa/factory/base.rb
+++ b/qa/qa/factory/base.rb
@@ -1,12 +1,19 @@
+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)
- Factory::Product.populate!(new) do |factory|
+ new.tap do |factory|
yield factory if block_given?
dependencies.each do |name, signature|
@@ -14,19 +21,37 @@ module QA
end
factory.fabricate!(*args)
+
+ return Factory::Product.populate!(self)
end
end
- def self.dependencies
- @dependencies ||= {}
+ def self.evaluator
+ @evaluator ||= Factory::Base::DSL.new(self)
end
- def self.dependency(factory, as:, &block)
- as.tap do |name|
- class_eval { attr_accessor name }
+ 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
- Dependency::Signature.new(factory, block).tap do |signature|
- dependencies.store(name, signature)
+ def product(attribute, &block)
+ Product::Attribute.new(attribute, block).tap do |signature|
+ @attributes.store(attribute, signature)
end
end
end
diff --git a/qa/qa/factory/dependency.rb b/qa/qa/factory/dependency.rb
index d0e85a68237..fc5dc82ce29 100644
--- a/qa/qa/factory/dependency.rb
+++ b/qa/qa/factory/dependency.rb
@@ -16,20 +16,21 @@ module QA
def build!
return if overridden?
- Builder.new(@signature).fabricate!.tap do |product|
+ Builder.new(@signature, @factory).fabricate!.tap do |product|
@factory.public_send("#{@name}=", product)
end
end
class Builder
- def initialize(signature)
+ 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)
+ @block&.call(factory, @caller_factory)
end
end
end
diff --git a/qa/qa/factory/product.rb b/qa/qa/factory/product.rb
index df35bbbb443..d004e642f9b 100644
--- a/qa/qa/factory/product.rb
+++ b/qa/qa/factory/product.rb
@@ -5,8 +5,9 @@ module QA
class Product
include Capybara::DSL
- def initialize(factory)
- @factory = factory
+ Attribute = Struct.new(:name, :block)
+
+ def initialize
@location = current_url
end
@@ -15,11 +16,13 @@ module QA
end
def self.populate!(factory)
- raise ArgumentError unless block_given?
-
- yield factory
-
- new(factory)
+ new.tap do |product|
+ factory.attributes.each_value do |attribute|
+ product.instance_exec(&attribute.block).tap do |value|
+ product.define_singleton_method(attribute.name) { value }
+ end
+ end
+ end
end
end
end
diff --git a/qa/qa/factory/resource/deploy_key.rb b/qa/qa/factory/resource/deploy_key.rb
index 7c58e70bcc4..ff0b4a46b77 100644
--- a/qa/qa/factory/resource/deploy_key.rb
+++ b/qa/qa/factory/resource/deploy_key.rb
@@ -4,6 +4,18 @@ module QA
class DeployKey < Factory::Base
attr_accessor :title, :key
+ product :title do
+ Page::Project::Settings::Repository.act do
+ expand_deploy_keys(&:key_title)
+ end
+ end
+
+ product :fingerprint do
+ Page::Project::Settings::Repository.act do
+ expand_deploy_keys(&:key_fingerprint)
+ end
+ end
+
dependency Factory::Resource::Project, as: :project do |project|
project.name = 'project-to-deploy'
project.description = 'project for adding deploy key test'
@@ -13,7 +25,7 @@ module QA
project.visit!
Page::Menu::Side.act do
- click_repository_setting
+ click_repository_settings
end
Page::Project::Settings::Repository.perform do |setting|
diff --git a/qa/qa/factory/resource/issue.rb b/qa/qa/factory/resource/issue.rb
new file mode 100644
index 00000000000..95f48e20b3e
--- /dev/null
+++ b/qa/qa/factory/resource/issue.rb
@@ -0,0 +1,32 @@
+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/merge_request.rb b/qa/qa/factory/resource/merge_request.rb
new file mode 100644
index 00000000000..ce04e904aaf
--- /dev/null
+++ b/qa/qa/factory/resource/merge_request.rb
@@ -0,0 +1,49 @@
+require 'securerandom'
+
+module QA
+ module Factory
+ module Resource
+ class MergeRequest < Factory::Base
+ attr_accessor :title,
+ :description,
+ :source_branch,
+ :target_branch
+
+ dependency Factory::Resource::Project, as: :project do |project|
+ project.name = 'project-with-merge-request'
+ end
+
+ dependency Factory::Repository::Push, as: :target do |push, factory|
+ push.project = factory.project
+ push.branch_name = "master:#{factory.target_branch}"
+ end
+
+ dependency Factory::Repository::Push, as: :source do |push, factory|
+ push.project = factory.project
+ push.branch_name = "#{factory.target_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"
+ 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.create_merge_request
+ end
+ 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
new file mode 100644
index 00000000000..514e3615d18
--- /dev/null
+++ b/qa/qa/factory/resource/personal_access_token.rb
@@ -0,0 +1,27 @@
+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
index 07c2e3086d1..7df2dc6618c 100644
--- a/qa/qa/factory/resource/project.rb
+++ b/qa/qa/factory/resource/project.rb
@@ -13,6 +13,10 @@ module QA
@description = 'My awesome project'
end
+ product :name do
+ Page::Project::Show.act { project_name }
+ end
+
def fabricate!
group.visit!
diff --git a/qa/qa/factory/resource/runner.rb b/qa/qa/factory/resource/runner.rb
new file mode 100644
index 00000000000..5f37f8ac2e9
--- /dev/null
+++ b/qa/qa/factory/resource/runner.rb
@@ -0,0 +1,42 @@
+require 'securerandom'
+
+module QA
+ module Factory
+ module Resource
+ class Runner < Factory::Base
+ attr_writer :name, :tags
+
+ 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 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.register!
+ 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
new file mode 100644
index 00000000000..54ef4d8d964
--- /dev/null
+++ b/qa/qa/factory/resource/secret_variable.rb
@@ -0,0 +1,41 @@
+module QA
+ module Factory
+ module Resource
+ class SecretVariable < Factory::Base
+ attr_accessor :key, :value
+
+ product :key do
+ Page::Project::Settings::CICD.act do
+ expand_secret_variables(&:variable_key)
+ end
+ end
+
+ product :value do
+ Page::Project::Settings::CICD.act do
+ expand_secret_variables(&:variable_value)
+ end
+ end
+
+ 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(key)
+ page.fill_variable_value(value)
+
+ page.add_variable
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/README.md b/qa/qa/page/README.md
new file mode 100644
index 00000000000..83710606d7c
--- /dev/null
+++ b/qa/qa/page/README.md
@@ -0,0 +1,122 @@
+# Page objects in GitLab QA
+
+In GitLab QA we are using a known pattern, called _Page Objects_.
+
+This means that we have built an abstraction for all GitLab pages that we use
+to drive GitLab QA scenarios. Whenever we do something on a page, like filling
+in a form, or clicking a button, we do that only through a page object
+associated with this area of GitLab.
+
+For example, when GitLab QA test harness signs in into GitLab, it needs to fill
+in a user login and user password. In order to do that, we have a class, called
+`Page::Main::Login` and `sign_in_using_credentials` methods, that is the only
+piece of the code, that has knowledge about `user_login` and `user_password`
+fields.
+
+## Why do we need that?
+
+We need page objects, because we need to reduce duplication and avoid problems
+whenever someone changes some selectors in GitLab's source code.
+
+Imagine that we have a hundred specs in GitLab QA, and we need to sign into
+GitLab each time, before we make assertions. Without a page object one would
+need to rely on volatile helpers or invoke Capybara methods directly. Imagine
+invoking `fill_in :user_login` in every `*_spec.rb` file / test example.
+
+When someone later changes `t.text_field :login` in the view associated with
+this page to `t.text_field :username` it will generate a different field
+identifier, what would effectively break all tests.
+
+Because we are using `Page::Main::Login.act { sign_in_using_credentials }`
+everywhere, when we want to sign into GitLab, the page object is the single
+source of truth, and we will need to update `fill_in :user_login`
+to `fill_in :user_username` only in a one place.
+
+## What problems did we have in the past?
+
+We do not run QA tests for every commit, because of performance reasons, and
+the time it would take to build packages and test everything.
+
+That is why when someone changes `t.text_field :login` to
+`t.text_field :username` in the _new session_ view we won't know about this
+change until our GitLab QA nightly pipeline fails, or until someone triggers
+`package-qa` action in their merge request.
+
+Obviously such a change would break all tests. We call this problem a _fragile
+tests problem_.
+
+In order to make GitLab QA more reliable and robust, we had to solve this
+problem by introducing coupling between GitLab CE / EE views and GitLab QA.
+
+## How did we solve fragile tests problem?
+
+Currently, when you add a new `Page::Base` derived class, you will also need to
+define all selectors that your page objects depends on.
+
+Whenever you push your code to CE / EE repository, `qa:selectors` sanity test
+job is going to be run as a part of a CI pipeline.
+
+This test is going to validate all page objects that we have implemented in
+`qa/page` directory. When it fails, you will be notified about missing
+or invalid views / selectors definition.
+
+## How to properly implement a page object?
+
+We have built a DSL to define coupling between a page object and GitLab views
+it is actually implemented by. See an example below.
+
+```ruby
+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"'
+ 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"'
+ end
+
+ # ...
+ end
+end
+```
+
+It is possible to use `element` DSL method without value, with a String value
+or with a Regexp.
+
+```ruby
+view 'app/views/my/view.html.haml' do
+ # Require `f.submit "Sign in"` to be present in `my/view.html.haml
+ element :my_button, 'f.submit "Sign in"'
+
+ # 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
+end
+```
+
+## Running the test locally
+
+During development, you can run the `qa:selectors` test by running
+
+```shell
+bin/qa Test::Sanity::Selectors
+```
+
+from within the `qa` directory.
+
+## Where to ask for help?
+
+If you need more information, ask for help on `#qa` channel on Slack (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.
diff --git a/qa/qa/page/admin/settings.rb b/qa/qa/page/admin/settings.rb
index 39e2f2062ad..1f646103e7f 100644
--- a/qa/qa/page/admin/settings.rb
+++ b/qa/qa/page/admin/settings.rb
@@ -2,6 +2,14 @@ module QA
module Page
module Admin
class Settings < Page::Base
+ view 'app/views/admin/application_settings/_form.html.haml' do
+ element :form_actions, '.form-actions'
+ element :submit, "submit 'Save'"
+ element :repository_storage, '%legend Repository Storage'
+ element :hashed_storage,
+ 'Create new projects using hashed storage paths'
+ end
+
def enable_hashed_storage
scroll_to 'legend', text: 'Repository Storage'
check 'Create new projects using hashed storage paths'
diff --git a/qa/qa/page/base.rb b/qa/qa/page/base.rb
index 99eba02b6e3..5c3af4b9115 100644
--- a/qa/qa/page/base.rb
+++ b/qa/qa/page/base.rb
@@ -5,21 +5,26 @@ module QA
class Base
include Capybara::DSL
include Scenario::Actable
+ extend SingleForwardable
+
+ def_delegators :evaluator, :view, :views
def refresh
visit current_url
end
- def wait(css = '.application', time: 60)
- Time.now.tap do |start|
- while Time.now - start < time
- break if page.has_css?(css, wait: 5)
+ def wait(max: 60, time: 1, reload: true)
+ start = Time.now
- refresh
- end
+ while Time.now - start < max
+ return true if yield
+
+ sleep(time)
+
+ refresh if reload
end
- yield if block_given?
+ false
end
def scroll_to(selector, text: nil)
@@ -37,9 +42,74 @@ module QA
page.within(selector) { yield } if block_given?
end
+ # Returns true if successfully GETs the given URL
+ # Useful because `page.status_code` is unsupported by our driver, and
+ # we don't have access to the `response` to use `have_http_status`.
+ def asset_exists?(url)
+ page.execute_script <<~JS
+ xhr = new XMLHttpRequest();
+ xhr.open('GET', '#{url}', true);
+ xhr.send();
+ JS
+
+ return false unless wait(time: 0.5, max: 60, reload: false) do
+ page.evaluate_script('xhr.readyState == XMLHttpRequest.DONE')
+ end
+
+ page.evaluate_script('xhr.status') == 200
+ end
+
+ def find_element(name)
+ find(element_selector_css(name))
+ end
+
+ def click_element(name)
+ find_element(name).click
+ end
+
+ def fill_element(name, content)
+ find_element(name).set(content)
+ end
+
+ def within_element(name)
+ page.within(element_selector_css(name)) do
+ yield
+ end
+ end
+
+ def element_selector_css(name)
+ Page::Element.new(name).selector_css
+ end
+
def self.path
raise NotImplementedError
end
+
+ def self.evaluator
+ @evaluator ||= Page::Base::DSL.new
+ end
+
+ def self.errors
+ if views.empty?
+ return ["Page class does not have views / elements defined!"]
+ end
+
+ views.map(&:errors).flatten
+ end
+
+ class DSL
+ attr_reader :views
+
+ def initialize
+ @views = []
+ end
+
+ def view(path, &block)
+ Page::View.evaluate(&block).tap do |view|
+ @views.push(Page::View.new(path, view.elements))
+ end
+ end
+ end
end
end
end
diff --git a/qa/qa/page/component/dropzone.rb b/qa/qa/page/component/dropzone.rb
new file mode 100644
index 00000000000..15bdc742fda
--- /dev/null
+++ b/qa/qa/page/component/dropzone.rb
@@ -0,0 +1,31 @@
+module QA
+ module Page
+ module Component
+ class Dropzone
+ attr_reader :page, :container
+
+ # page - A QA::Page::Base object
+ # container - CSS selector of the comment textarea's container
+ def initialize(page, container)
+ @page = page
+ @container = container
+ end
+
+ # Not tested and not expected to work with multiple dropzones
+ # instantiated on one page because there is no distinguishing
+ # attribute per dropzone file field.
+ def attach_file(attachment)
+ filename = File.basename(attachment)
+
+ field_style = { visibility: 'visible', height: '', width: '' }
+ page.attach_file(attachment, class: 'dz-hidden-input', make_visible: field_style)
+
+ # Wait for link to be appended to dropzone text
+ page.wait(reload: false) do
+ page.find("#{container} textarea").value.match(filename)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/dashboard/groups.rb b/qa/qa/page/dashboard/groups.rb
index 083d2e1ab16..e853e0d85e0 100644
--- a/qa/qa/page/dashboard/groups.rb
+++ b/qa/qa/page/dashboard/groups.rb
@@ -2,6 +2,15 @@ module QA
module Page
module Dashboard
class Groups < Page::Base
+ view 'app/views/shared/groups/_search_form.html.haml' do
+ element :groups_filter, 'search_field_tag :filter'
+ element :groups_filter_placeholder, 'Filter by name...'
+ end
+
+ view 'app/views/dashboard/_groups_head.html.haml' do
+ element :new_group_button, 'link_to _("New group")'
+ end
+
def filter_by_name(name)
fill_in 'Filter by name...', with: name
end
diff --git a/qa/qa/page/dashboard/projects.rb b/qa/qa/page/dashboard/projects.rb
index 7ed27da6d89..73942cb856a 100644
--- a/qa/qa/page/dashboard/projects.rb
+++ b/qa/qa/page/dashboard/projects.rb
@@ -2,9 +2,22 @@ module QA
module Page
module Dashboard
class Projects < Page::Base
+ 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'/
+ end
+
def go_to_project(name)
+ filter_by_name(name)
+
find_link(text: name).click
end
+
+ def filter_by_name(name)
+ page.within('form#project-filter-form') do
+ fill_in :name, with: name
+ end
+ end
end
end
end
diff --git a/qa/qa/page/element.rb b/qa/qa/page/element.rb
new file mode 100644
index 00000000000..9944a39ce07
--- /dev/null
+++ b/qa/qa/page/element.rb
@@ -0,0 +1,32 @@
+module QA
+ module Page
+ class Element
+ attr_reader :name
+
+ def initialize(name, pattern = nil)
+ @name = name
+ @pattern = pattern || selector
+ end
+
+ def selector
+ "qa-#{@name.to_s.tr('_', '-')}"
+ end
+
+ def selector_css
+ ".#{selector}"
+ end
+
+ def expression
+ if @pattern.is_a?(String)
+ @_regexp ||= Regexp.new(Regexp.escape(@pattern))
+ else
+ @pattern
+ end
+ end
+
+ def matches?(line)
+ !!(line =~ expression)
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/group/new.rb b/qa/qa/page/group/new.rb
index 53fdaaed078..48b71a7c883 100644
--- a/qa/qa/page/group/new.rb
+++ b/qa/qa/page/group/new.rb
@@ -2,6 +2,17 @@ module QA
module Page
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'
+ end
+
+ view 'app/views/groups/new.html.haml' do
+ element :create_group_button, "submit 'Create group'"
+ element :visibility_radios, 'visibility_level:'
+ end
+
def set_path(path)
fill_in 'group_path', with: path
fill_in 'group_name', with: path
diff --git a/qa/qa/page/group/show.rb b/qa/qa/page/group/show.rb
index 0a16c07d64b..d215518d316 100644
--- a/qa/qa/page/group/show.rb
+++ b/qa/qa/page/group/show.rb
@@ -2,6 +2,21 @@ module QA
module Page
module Group
class Show < Page::Base
+ 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/
+ end
+
+ view 'app/assets/javascripts/groups/constants.js' do
+ element :no_result_text, 'Sorry, no groups or projects matched your search'
+ end
+
def go_to_subgroup(name)
click_link name
end
@@ -13,26 +28,41 @@ module QA
def has_subgroup?(name)
filter_by_name(name)
- page.has_link?(name)
+ wait(reload: false) do
+ return false if page.has_content?('Sorry, no groups or projects matched your search')
+
+ page.has_link?(name)
+ end
end
def go_to_new_subgroup
- within '.new-project-subgroup' do
- find('.dropdown-toggle').click
- find("li[data-value='new-subgroup']").click
- end
+ click_new('subgroup')
find("input[data-action='new-subgroup']").click
end
def go_to_new_project
- within '.new-project-subgroup' do
- find('.dropdown-toggle').click
- find("li[data-value='new-project']").click
- end
+ click_new('project')
find("input[data-action='new-project']").click
end
+
+ private
+
+ def click_new(kind)
+ within '.new-project-subgroup' do
+ css = "li[data-value='new-#{kind}']"
+
+ # 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
+
+ page.has_css?(css)
+ end
+
+ find(css).click
+ end
+ end
end
end
end
diff --git a/qa/qa/page/main/login.rb b/qa/qa/page/main/login.rb
index f88325f408b..95880475ffa 100644
--- a/qa/qa/page/main/login.rb
+++ b/qa/qa/page/main/login.rb
@@ -2,20 +2,36 @@ module QA
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"'
+ 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"'
+ end
+
def initialize
- wait('.application', time: 500)
+ wait(max: 500) do
+ page.has_css?('.application')
+ end
end
def sign_in_using_credentials
- if page.has_content?('Change your password')
+ using_wait_time 0 do
+ if page.has_content?('Change your password')
+ fill_in :user_password, with: Runtime::User.password
+ fill_in :user_password_confirmation, with: Runtime::User.password
+ click_button 'Change your password'
+ end
+
+ fill_in :user_login, with: Runtime::User.name
fill_in :user_password, with: Runtime::User.password
- fill_in :user_password_confirmation, with: Runtime::User.password
- click_button 'Change your password'
+ click_button 'Sign in'
end
-
- fill_in :user_login, with: Runtime::User.name
- fill_in :user_password, with: Runtime::User.password
- click_button 'Sign in'
end
def self.path
diff --git a/qa/qa/page/main/oauth.rb b/qa/qa/page/main/oauth.rb
index e746cff0a80..6f548148363 100644
--- a/qa/qa/page/main/oauth.rb
+++ b/qa/qa/page/main/oauth.rb
@@ -2,6 +2,10 @@ module QA
module Page
module Main
class OAuth < Page::Base
+ view 'app/views/doorkeeper/authorizations/new.html.haml' do
+ element :authorization_button, 'submit_tag "Authorize"'
+ end
+
def needs_authorization?
page.current_url.include?('/oauth')
end
diff --git a/qa/qa/page/mattermost/login.rb b/qa/qa/page/mattermost/login.rb
index 8ffd4fdad13..9b21300ea3c 100644
--- a/qa/qa/page/mattermost/login.rb
+++ b/qa/qa/page/mattermost/login.rb
@@ -2,6 +2,13 @@ module QA
module Page
module Mattermost
class Login < Page::Base
+ ##
+ # TODO, define all selectors required by this page object
+ #
+ # See gitlab-org/gitlab-qa#154
+ #
+ view 'app/views/projects/mattermosts/new.html.haml'
+
def sign_in_using_oauth
click_link class: 'btn btn-custom-login gitlab'
diff --git a/qa/qa/page/mattermost/main.rb b/qa/qa/page/mattermost/main.rb
index 4b8fc28e53f..bc2f9acc729 100644
--- a/qa/qa/page/mattermost/main.rb
+++ b/qa/qa/page/mattermost/main.rb
@@ -2,6 +2,13 @@ module QA
module Page
module Mattermost
class Main < Page::Base
+ ##
+ # TODO, define all selectors required by this page object
+ #
+ # See gitlab-org/gitlab-qa#154
+ #
+ view 'app/views/projects/mattermosts/new.html.haml'
+
def initialize
visit(Runtime::Scenario.mattermost_address)
end
diff --git a/qa/qa/page/menu/admin.rb b/qa/qa/page/menu/admin.rb
index 07fe40fda3a..573b98f7386 100644
--- a/qa/qa/page/menu/admin.rb
+++ b/qa/qa/page/menu/admin.rb
@@ -2,8 +2,8 @@ module QA
module Page
module Menu
class Admin < Page::Base
- def go_to_license
- click_link 'License'
+ view 'app/views/layouts/nav/sidebar/_admin.html.haml' do
+ element :settings, "_('Settings')"
end
def go_to_settings
diff --git a/qa/qa/page/menu/main.rb b/qa/qa/page/menu/main.rb
index b94c2c6c23d..df93a5fa2d2 100644
--- a/qa/qa/page/menu/main.rb
+++ b/qa/qa/page/menu/main.rb
@@ -2,42 +2,70 @@ module QA
module Page
module Menu
class Main < Page::Base
+ view 'app/views/layouts/header/_default.html.haml' do
+ element :navbar
+ element :user_avatar
+ element :user_menu, '.dropdown-menu-nav'
+ element :user_sign_out_link, 'link_to "Sign out"'
+ element :settings_link, 'link_to "Settings"'
+ end
+
+ view 'app/views/layouts/nav/_dashboard.html.haml' do
+ element :admin_area_link
+ element :projects_dropdown
+ element :groups_link
+ end
+
+ view 'app/views/layouts/nav/projects_dropdown/_show.html.haml' do
+ element :projects_dropdown_sidebar
+ element :your_projects_link
+ end
+
def go_to_groups
- within_top_menu { click_link 'Groups' }
+ within_top_menu { click_element :groups_link }
end
def go_to_projects
within_top_menu do
- click_link 'Projects'
- click_link 'Your projects'
+ click_element :projects_dropdown
+ end
+
+ page.within('.qa-projects-dropdown-sidebar') do
+ click_element :your_projects_link
end
end
def go_to_admin_area
- within_top_menu { find('.admin-icon').click }
+ within_top_menu { click_element :admin_area_link }
end
def sign_out
within_user_menu do
- click_link('Sign out')
+ click_link 'Sign out'
+ end
+ end
+
+ def go_to_profile_settings
+ within_user_menu do
+ click_link 'Settings'
end
end
def has_personal_area?
- page.has_selector?('.header-user-dropdown-toggle')
+ page.has_selector?('.qa-user-avatar')
end
private
def within_top_menu
- page.within('.navbar') do
+ page.within('.qa-navbar') do
yield
end
end
def within_user_menu
within_top_menu do
- find('.header-user-dropdown-toggle').click
+ click_element :user_avatar
page.within('.dropdown-menu-nav') do
yield
diff --git a/qa/qa/page/menu/profile.rb b/qa/qa/page/menu/profile.rb
new file mode 100644
index 00000000000..95e88d863e4
--- /dev/null
+++ b/qa/qa/page/menu/profile.rb
@@ -0,0 +1,27 @@
+module QA
+ module Page
+ module Menu
+ class Profile < 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'
+ end
+
+ def click_access_tokens
+ within_sidebar do
+ click_link('Access Tokens')
+ end
+ end
+
+ private
+
+ def within_sidebar
+ page.within('.sidebar-top-level-items') do
+ yield
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/menu/side.rb b/qa/qa/page/menu/side.rb
index 6c25aba4bac..7e028add2ef 100644
--- a/qa/qa/page/menu/side.rb
+++ b/qa/qa/page/menu/side.rb
@@ -2,17 +2,60 @@ module QA
module Page
module Menu
class Side < Page::Base
- def click_repository_setting
- hover_setting do
- click_link('Repository')
+ 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 :pipelines_settings_link, "title: 'CI / CD'"
+ element :issues_link, /link_to.*shortcuts-issues/
+ element :issues_link_text, "Issues"
+ element :top_level_items, '.sidebar-top-level-items'
+ element :activity_link, "title: 'Activity'"
+ end
+
+ view 'app/assets/javascripts/fly_out_nav.js' do
+ element :fly_out, "classList.add('fly-out-list')"
+ end
+
+ def click_repository_settings
+ hover_settings do
+ within_submenu do
+ click_link('Repository')
+ end
+ end
+ end
+
+ def click_ci_cd_settings
+ hover_settings do
+ within_submenu do
+ click_link('CI / CD')
+ end
+ end
+ end
+
+ def click_ci_cd_pipelines
+ within_sidebar do
+ click_link('CI / CD')
+ end
+ end
+
+ def go_to_settings
+ within_sidebar do
+ click_on 'Settings'
+ end
+ end
+
+ def click_issues
+ within_sidebar do
+ click_link('Issues')
end
end
private
- def hover_setting
+ def hover_settings
within_sidebar do
- find('.nav-item-name', text: 'Settings').hover
+ find('.qa-settings-item').hover
yield
end
@@ -23,6 +66,18 @@ module QA
yield
end
end
+
+ def go_to_activity
+ within_sidebar do
+ click_on 'Activity'
+ end
+ end
+
+ def within_submenu
+ page.within('.fly-out-list') do
+ yield
+ end
+ end
end
end
end
diff --git a/qa/qa/page/merge_request/new.rb b/qa/qa/page/merge_request/new.rb
new file mode 100644
index 00000000000..ec94ff4ac98
--- /dev/null
+++ b/qa/qa/page/merge_request/new.rb
@@ -0,0 +1,31 @@
+module QA
+ module Page
+ module MergeRequest
+ class New < Page::Base
+ view 'app/views/shared/issuable/_form.html.haml' do
+ element :issuable_create_button
+ end
+
+ view 'app/views/shared/issuable/form/_title.html.haml' do
+ element :issuable_form_title
+ end
+
+ view 'app/views/shared/form_elements/_description.html.haml' do
+ element :issuable_form_description
+ end
+
+ def create_merge_request
+ click_element :issuable_create_button
+ end
+
+ def fill_title(title)
+ fill_element :issuable_form_title, title
+ end
+
+ def fill_description(description)
+ fill_element :issuable_form_description, description
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/profile/personal_access_tokens.rb b/qa/qa/page/profile/personal_access_tokens.rb
new file mode 100644
index 00000000000..f5ae47dadd0
--- /dev/null
+++ b/qa/qa/page/profile/personal_access_tokens.rb
@@ -0,0 +1,33 @@
+module QA
+ module Page
+ 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"
+ end
+
+ view 'app/views/profiles/personal_access_tokens/index.html.haml' do
+ element :create_token_field, "text_field_tag 'created-personal-access-token'"
+ end
+
+ def fill_token_name(name)
+ fill_in 'personal_access_token_name', with: name
+ end
+
+ def check_api
+ check 'personal_access_token_scopes_api'
+ end
+
+ def create_token
+ click_on 'Create personal access token'
+ end
+
+ def created_access_token
+ page.find('#created-personal-access-token').value
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/project/activity.rb b/qa/qa/page/project/activity.rb
new file mode 100644
index 00000000000..0196922c889
--- /dev/null
+++ b/qa/qa/page/project/activity.rb
@@ -0,0 +1,15 @@
+module QA
+ module Page
+ 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')"
+ end
+
+ def go_to_push_events
+ click_on 'Push events'
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/project/issue/index.rb b/qa/qa/page/project/issue/index.rb
new file mode 100644
index 00000000000..b5903f536a4
--- /dev/null
+++ b/qa/qa/page/project/issue/index.rb
@@ -0,0 +1,17 @@
+module QA
+ module Page
+ module Project
+ module Issue
+ class Index < Page::Base
+ view 'app/views/projects/issues/_issue.html.haml' do
+ element :issue_link, 'link_to issue.title'
+ end
+
+ def go_to_issue(title)
+ click_link(title)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/project/issue/new.rb b/qa/qa/page/project/issue/new.rb
new file mode 100644
index 00000000000..7fc581da1ed
--- /dev/null
+++ b/qa/qa/page/project/issue/new.rb
@@ -0,0 +1,33 @@
+module QA
+ module Page
+ module Project
+ module Issue
+ class New < Page::Base
+ view 'app/views/shared/issuable/_form.html.haml' do
+ element :submit_issue_button, 'form.submit "Submit'
+ end
+
+ view 'app/views/shared/issuable/form/_title.html.haml' do
+ element :issue_title_textbox, 'form.text_field :title'
+ end
+
+ view 'app/views/shared/form_elements/_description.html.haml' do
+ element :issue_description_textarea, "render 'projects/zen', f: form, attr: :description"
+ end
+
+ def add_title(title)
+ fill_in 'issue_title', with: title
+ end
+
+ def add_description(description)
+ fill_in 'issue_description', with: description
+ end
+
+ def create_new_issue
+ click_on 'Submit issue'
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/project/issue/show.rb b/qa/qa/page/project/issue/show.rb
new file mode 100644
index 00000000000..5bc0598a524
--- /dev/null
+++ b/qa/qa/page/project/issue/show.rb
@@ -0,0 +1,40 @@
+module QA
+ module Page
+ 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
+
+ view 'app/views/shared/notes/_form.html.haml' do
+ element :new_note_form, 'new-note'
+ element :new_note_form, 'attr: :note'
+ end
+
+ view 'app/views/shared/notes/_comment_button.html.haml' do
+ element :comment_button, '%strong Comment'
+ end
+
+ def issue_title
+ find('.issue-details .title').text
+ 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]')
+
+ unless attachment.nil?
+ QA::Page::Component::Dropzone.new(self, '.new-note')
+ .attach_file(attachment)
+ end
+
+ click_on 'Comment'
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/project/new.rb b/qa/qa/page/project/new.rb
index b31bec27b59..186a4724326 100644
--- a/qa/qa/page/project/new.rb
+++ b/qa/qa/page/project/new.rb
@@ -2,9 +2,18 @@ module QA
module Page
module Project
class New < Page::Base
+ view 'app/views/projects/_new_project_fields.html.haml' do
+ element :project_namespace_select
+ element :project_namespace_field, /select :namespace_id.*class: 'select2/
+ element :project_path, 'text_field :path'
+ element :project_description, 'text_area :description'
+ element :project_create_button, "submit 'Create project'"
+ end
+
def choose_test_namespace
- find('#s2id_project_namespace_id').click
- find('.select2-result-label', text: Runtime::Namespace.name).click
+ click_element :project_namespace_select
+
+ find('ul.select2-result-sub > li', text: Runtime::Namespace.path).click
end
def choose_name(name)
diff --git a/qa/qa/page/project/pipeline/index.rb b/qa/qa/page/project/pipeline/index.rb
new file mode 100644
index 00000000000..32c108393b9
--- /dev/null
+++ b/qa/qa/page/project/pipeline/index.rb
@@ -0,0 +1,13 @@
+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"'
+ end
+
+ def go_to_latest_pipeline
+ first('.js-pipeline-url-link').click
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/project/pipeline/show.rb b/qa/qa/page/project/pipeline/show.rb
new file mode 100644
index 00000000000..0835173f1cd
--- /dev/null
+++ b/qa/qa/page/project/pipeline/show.rb
@@ -0,0 +1,35 @@
+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.*/
+ end
+
+ view 'app/assets/javascripts/pipelines/components/graph/graph_component.vue' do
+ element :pipeline_graph, /class.*pipeline-graph.*/
+ end
+
+ view 'app/assets/javascripts/pipelines/components/graph/job_component.vue' do
+ element :job_component, /class.*ci-job-component.*/
+ end
+
+ view 'app/assets/javascripts/vue_shared/components/ci_icon.vue' do
+ element :status_icon, 'ci-status-icon-${status}'
+ end
+
+ def running?
+ within('.ci-header-container') do
+ return page.has_content?('running')
+ end
+ end
+
+ def has_build?(name, status: :success)
+ within('.pipeline-graph') do
+ within('.ci-job-component', text: name) do
+ return has_selector?(".ci-status-icon-#{status}")
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/project/settings/advanced.rb b/qa/qa/page/project/settings/advanced.rb
new file mode 100644
index 00000000000..5ef00504fdf
--- /dev/null
+++ b/qa/qa/page/project/settings/advanced.rb
@@ -0,0 +1,33 @@
+module QA
+ module Page
+ module Project
+ module Settings
+ class Advanced < Page::Base
+ view 'app/views/projects/edit.html.haml' do
+ element :project_path_field, 'f.text_field :path'
+ element :project_name_field, 'f.text_field :name'
+ element :rename_project_button, "f.submit 'Rename project'"
+ end
+
+ def rename_to(path)
+ fill_project_name(path)
+ fill_project_path(path)
+ rename_project!
+ end
+
+ def fill_project_path(path)
+ fill_in :project_path, with: path
+ end
+
+ def fill_project_name(name)
+ fill_in :project_name, with: name
+ end
+
+ def rename_project!
+ click_on 'Rename project'
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/project/settings/ci_cd.rb b/qa/qa/page/project/settings/ci_cd.rb
new file mode 100644
index 00000000000..99be21bbe89
--- /dev/null
+++ b/qa/qa/page/project/settings/ci_cd.rb
@@ -0,0 +1,28 @@
+module QA
+ module Page
+ module Project
+ module Settings
+ class CICD < Page::Base
+ include Common
+
+ view 'app/views/projects/settings/ci_cd/show.html.haml' do
+ element :runners_settings, 'Runners settings'
+ element :secret_variables, 'Secret variables'
+ end
+
+ def expand_runners_settings(&block)
+ expand_section('Runners settings') do
+ Settings::Runners.perform(&block)
+ end
+ end
+
+ def expand_secret_variables(&block)
+ expand_section('Secret variables') do
+ Settings::SecretVariables.perform(&block)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/project/settings/common.rb b/qa/qa/page/project/settings/common.rb
index 5d1d5120929..319cb1045b6 100644
--- a/qa/qa/page/project/settings/common.rb
+++ b/qa/qa/page/project/settings/common.rb
@@ -3,11 +3,29 @@ module QA
module Project
module Settings
module Common
- def expand(selector)
+ def self.included(base)
+ base.class_eval do
+ view 'app/views/projects/edit.html.haml' do
+ element :advanced_settings_expand, "= expanded ? 'Collapse' : 'Expand'"
+ end
+ end
+ end
+
+ # Click the Expand button present in the specified section
+ #
+ # @param [String] name present in the container in the DOM
+ def expand_section(name)
page.within('#content-body') do
- find(selector).click
+ page.within('section', text: name) do
+ # Because it is possible to click the button before the JS toggle code is bound
+ wait(reload: false) do
+ click_button 'Expand' unless first('button', text: 'Collapse')
+
+ page.has_content?('Collapse')
+ end
- yield
+ yield if block_given?
+ end
end
end
end
diff --git a/qa/qa/page/project/settings/deploy_keys.rb b/qa/qa/page/project/settings/deploy_keys.rb
index 4028b8cccc5..332e84724c7 100644
--- a/qa/qa/page/project/settings/deploy_keys.rb
+++ b/qa/qa/page/project/settings/deploy_keys.rb
@@ -3,6 +3,21 @@ module QA
module Project
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'
+ 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"'
+ 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.*"/
+ end
+
def fill_key_title(title)
fill_in 'deploy_key_title', with: title
end
@@ -15,9 +30,23 @@ module QA
click_on 'Add key'
end
- def has_key_title?(title)
- page.within('.deploy-keys') do
- page.find('.title', text: title)
+ def key_title
+ within_project_deploy_keys do
+ find_element(:key_title).text
+ end
+ end
+
+ def key_fingerprint
+ within_project_deploy_keys do
+ find_element(:key_fingerprint).text
+ end
+ end
+
+ private
+
+ def within_project_deploy_keys
+ within_element(:project_deploy_keys) do
+ yield
end
end
end
diff --git a/qa/qa/page/project/settings/main.rb b/qa/qa/page/project/settings/main.rb
new file mode 100644
index 00000000000..5d743f4c9c8
--- /dev/null
+++ b/qa/qa/page/project/settings/main.rb
@@ -0,0 +1,21 @@
+module QA
+ module Page
+ module Project
+ module Settings
+ class Main < Page::Base
+ include Common
+
+ view 'app/views/projects/edit.html.haml' do
+ element :advanced_settings_section, 'Advanced settings'
+ end
+
+ def expand_advanced_settings(&block)
+ expand_section('Advanced settings') do
+ Advanced.perform(&block)
+ end
+ 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 034b0d09c1c..22362164a1a 100644
--- a/qa/qa/page/project/settings/repository.rb
+++ b/qa/qa/page/project/settings/repository.rb
@@ -5,8 +5,12 @@ module QA
class Repository < Page::Base
include Common
+ view 'app/views/projects/deploy_keys/_index.html.haml' do
+ element :deploy_keys_section, 'Deploy Keys'
+ end
+
def expand_deploy_keys(&block)
- expand('.qa-expand-deploy-keys') do
+ expand_section('Deploy Keys') do
DeployKeys.perform(&block)
end
end
diff --git a/qa/qa/page/project/settings/runners.rb b/qa/qa/page/project/settings/runners.rb
new file mode 100644
index 00000000000..b41668c94cd
--- /dev/null
+++ b/qa/qa/page/project/settings/runners.rb
@@ -0,0 +1,35 @@
+module QA
+ module Page
+ module Project
+ 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'
+ end
+
+ ##
+ # TODO, phase-out CSS classes added in Ruby helpers.
+ #
+ view 'app/helpers/runners_helper.rb' do
+ # rubocop:disable Lint/InterpolationCheck
+ element :runner_status, 'runner-status-#{status}'
+ # rubocop:enable Lint/InterpolationCheck
+ end
+
+ def registration_token
+ find('code#registration_token').text
+ end
+
+ def coordinator_address
+ find('code#coordinator_address').text
+ end
+
+ def has_online_runner?
+ page.has_css?('.runner-status-online')
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/project/settings/secret_variables.rb b/qa/qa/page/project/settings/secret_variables.rb
new file mode 100644
index 00000000000..e3bfbfcf080
--- /dev/null
+++ b/qa/qa/page/project/settings/secret_variables.rb
@@ -0,0 +1,57 @@
+module QA
+ module Page
+ module Project
+ module Settings
+ class SecretVariables < Page::Base
+ include Common
+
+ view 'app/views/ci/variables/_table.html.haml' do
+ element :variable_key, '.variable-key'
+ element :variable_value, '.variable-value'
+ end
+
+ view 'app/views/ci/variables/_index.html.haml' do
+ element :add_new_variable, 'btn_text: "Add new variable"'
+ end
+
+ view 'app/assets/javascripts/behaviors/secret_values.js' do
+ element :reveal_value, 'Reveal value'
+ element :hide_value, 'Hide value'
+ end
+
+ def fill_variable_key(key)
+ fill_in 'variable_key', with: key
+ end
+
+ def fill_variable_value(value)
+ fill_in 'variable_value', with: value
+ end
+
+ def add_variable
+ click_on 'Add new variable'
+ end
+
+ def variable_key
+ page.find('.variable-key').text
+ end
+
+ def variable_value
+ reveal_value do
+ page.find('.variable-value').text
+ end
+ end
+
+ private
+
+ def reveal_value
+ click_button('Reveal value')
+
+ yield.tap do
+ click_button('Hide value')
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/project/show.rb b/qa/qa/page/project/show.rb
index 3b2bac84f3f..553d35f9579 100644
--- a/qa/qa/page/project/show.rb
+++ b/qa/qa/page/project/show.rb
@@ -2,11 +2,36 @@ module QA
module Page
module Project
class Show < Page::Base
+ view 'app/views/shared/_clone_panel.html.haml' do
+ element :clone_holder, '.git-clone-holder'
+ element :clone_dropdown
+ element :clone_options_dropdown, '.clone-options-dropdown'
+ element :project_repository_location, 'text_field_tag :project_clone'
+ end
+
+ view 'app/views/projects/_last_push.html.haml' do
+ element :create_merge_request
+ end
+
+ view 'app/views/projects/_home_panel.html.haml' do
+ element :project_name
+ end
+
+ 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)"
+ end
+
def choose_repository_clone_http
- find('#clone-dropdown').click
+ wait(reload: false) do
+ click_element :clone_dropdown
+
+ page.within('.clone-options-dropdown') do
+ click_link('HTTP')
+ end
- page.within('.clone-options-dropdown') do
- click_link('HTTP')
+ # Ensure git clone textbox was updated to http URI
+ page.has_css?('.git-clone-holder input#project_clone[value*="http"]')
end
end
@@ -15,11 +40,22 @@ module QA
end
def project_name
- find('.project-title').text
+ find('.qa-project-name').text
+ end
+
+ def new_merge_request
+ click_element :create_merge_request
end
def wait_for_push
sleep 5
+ refresh
+ end
+
+ def go_to_new_issue
+ click_element :new_menu_toggle
+
+ click_link 'New issue'
end
end
end
diff --git a/qa/qa/page/validator.rb b/qa/qa/page/validator.rb
new file mode 100644
index 00000000000..117d8d4db67
--- /dev/null
+++ b/qa/qa/page/validator.rb
@@ -0,0 +1,52 @@
+module QA
+ module Page
+ class Validator
+ ValidationError = Class.new(StandardError)
+
+ Error = Struct.new(:page, :message) do
+ def to_s
+ "Error: #{page} - #{message}"
+ end
+ end
+
+ def initialize(constant)
+ @module = constant
+ end
+
+ def constants
+ @consts ||= @module.constants.map do |const|
+ @module.const_get(const)
+ end
+ end
+
+ def descendants
+ @descendants ||= constants.map do |const|
+ case const
+ when Class
+ const if const < Page::Base
+ when Module
+ Page::Validator.new(const).descendants
+ end
+ end
+
+ @descendants.flatten.compact
+ end
+
+ def errors
+ [].tap do |errors|
+ descendants.each do |page|
+ page.errors.each do |message|
+ errors.push(Error.new(page.name, message))
+ end
+ end
+ end
+ end
+
+ def validate!
+ return if errors.none?
+
+ raise ValidationError, 'Page views / elements validation error!'
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/view.rb b/qa/qa/page/view.rb
new file mode 100644
index 00000000000..6635e1ce039
--- /dev/null
+++ b/qa/qa/page/view.rb
@@ -0,0 +1,55 @@
+module QA
+ module Page
+ class View
+ attr_reader :path, :elements
+
+ def initialize(path, elements)
+ @path = path
+ @elements = elements
+ end
+
+ def pathname
+ @pathname ||= Pathname.new(File.join(__dir__, '../../../', @path))
+ .cleanpath.expand_path
+ end
+
+ def errors
+ unless pathname.readable?
+ return ["Missing view partial `#{pathname}`!"]
+ end
+
+ ##
+ # Reduce required elements by streaming view and making assertions on
+ # elements' existence.
+ #
+ @missing ||= @elements.dup.tap do |elements|
+ File.foreach(pathname.to_s) do |line|
+ elements.reject! { |element| element.matches?(line) }
+ end
+ end
+
+ @missing.map do |missing|
+ "Missing element `#{missing.name}` in `#{pathname}` view partial!"
+ end
+ end
+
+ def self.evaluate(&block)
+ Page::View::DSL.new.tap do |evaluator|
+ evaluator.instance_exec(&block) if block_given?
+ end
+ end
+
+ class DSL
+ attr_reader :elements
+
+ def initialize
+ @elements = []
+ end
+
+ def element(name, pattern = nil)
+ @elements.push(Page::Element.new(name, pattern))
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/runtime/address.rb b/qa/qa/runtime/address.rb
new file mode 100644
index 00000000000..ffad3974b02
--- /dev/null
+++ b/qa/qa/runtime/address.rb
@@ -0,0 +1,20 @@
+module QA
+ module Runtime
+ class Address
+ attr_reader :address
+
+ def initialize(instance, page = nil)
+ @instance = instance
+ @address = host + (page.is_a?(String) ? page : page&.path)
+ end
+
+ def host
+ if @instance.is_a?(Symbol)
+ Runtime::Scenario.send("#{@instance}_address")
+ else
+ @instance.to_s
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/runtime/api.rb b/qa/qa/runtime/api.rb
new file mode 100644
index 00000000000..e2a096b971d
--- /dev/null
+++ b/qa/qa/runtime/api.rb
@@ -0,0 +1,82 @@
+require 'airborne'
+
+module QA
+ module Runtime
+ module API
+ class Client
+ attr_reader :address
+
+ def initialize(address = :gitlab)
+ @address = address
+ 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
+ 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
+ end
+ end
+ end
+
+ class Request
+ API_VERSION = 'v4'.freeze
+
+ def initialize(api_client, path, personal_access_token: nil)
+ personal_access_token ||= api_client.personal_access_token
+ request_path = request_path(path, personal_access_token: personal_access_token)
+ @session_address = Runtime::Address.new(api_client.address, request_path)
+ end
+
+ def url
+ @session_address.address
+ end
+
+ # Prepend a request path with the path to the API
+ #
+ # path - Path to append
+ #
+ # Examples
+ #
+ # >> request_path('/issues')
+ # => "/api/v4/issues"
+ #
+ # >> request_path('/issues', personal_access_token: 'sometoken)
+ # => "/api/v4/issues?private_token=..."
+ #
+ # Returns the relative path to the requested API resource
+ def request_path(path, version: API_VERSION, personal_access_token: nil, oauth_access_token: nil)
+ full_path = File.join('/api', version, path)
+
+ if oauth_access_token
+ query_string = "access_token=#{oauth_access_token}"
+ elsif personal_access_token
+ query_string = "private_token=#{personal_access_token}"
+ end
+
+ if query_string
+ full_path << (path.include?('?') ? '&' : '?')
+ full_path << query_string
+ end
+
+ full_path
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/runtime/browser.rb b/qa/qa/runtime/browser.rb
index 14b2a488760..a12d95683af 100644
--- a/qa/qa/runtime/browser.rb
+++ b/qa/qa/runtime/browser.rb
@@ -23,13 +23,11 @@ module QA
# In case of an address that is a symbol we will try to guess address
# based on `Runtime::Scenario#something_address`.
#
- def visit(address, page, &block)
- Browser::Session.new(address, page).tap do |session|
- session.perform(&block)
- end
+ def visit(address, page = nil, &block)
+ Browser::Session.new(address, page).perform(&block)
end
- def self.visit(address, page, &block)
+ def self.visit(address, page = nil, &block)
new.visit(address, page, &block)
end
@@ -86,7 +84,7 @@ module QA
config.javascript_driver = :chrome
config.default_max_wait_time = 10
# https://github.com/mattheworiordan/capybara-screenshot/issues/164
- config.save_path = 'tmp'
+ config.save_path = File.expand_path('../../tmp', __dir__)
end
end
@@ -94,20 +92,15 @@ module QA
include Capybara::DSL
def initialize(instance, page = nil)
- @instance = instance
- @address = host + page&.path
+ @session_address = Runtime::Address.new(instance, page)
end
- def host
- if @instance.is_a?(Symbol)
- Runtime::Scenario.send("#{@instance}_address")
- else
- @instance.to_s
- end
+ def url
+ @session_address.address
end
def perform(&block)
- visit(@address)
+ visit(url)
yield if block_given?
rescue
@@ -130,7 +123,7 @@ module QA
# See gitlab-org/gitlab-qa#102
#
def clear!
- visit(@address)
+ visit(url)
reset_session!
end
end
diff --git a/qa/qa/runtime/env.rb b/qa/qa/runtime/env.rb
index d5c28e9a7db..56944e8b641 100644
--- a/qa/qa/runtime/env.rb
+++ b/qa/qa/runtime/env.rb
@@ -3,6 +3,7 @@ module QA
module Env
extend self
+ # set to 'false' to have Chrome run visibly instead of headless
def chrome_headless?
(ENV['CHROME_HEADLESS'] =~ /^(false|no|0)$/i) != 0
end
@@ -10,6 +11,11 @@ module QA
def running_in_ci?
ENV['CI'] || ENV['CI_SERVER']
end
+
+ # specifies token that can be used for the api
+ def personal_access_token
+ ENV['PERSONAL_ACCESS_TOKEN']
+ end
end
end
end
diff --git a/qa/qa/runtime/namespace.rb b/qa/qa/runtime/namespace.rb
index b00e925986b..a72c2d21898 100644
--- a/qa/qa/runtime/namespace.rb
+++ b/qa/qa/runtime/namespace.rb
@@ -11,6 +11,10 @@ module QA
'qa-test-' + time.strftime('%d-%m-%Y-%H-%M-%S')
end
+ def path
+ "#{sandbox_name}/#{name}"
+ end
+
def sandbox_name
'gitlab-qa-sandbox'
end
diff --git a/qa/qa/runtime/rsa_key.rb b/qa/qa/runtime/rsa_key.rb
new file mode 100644
index 00000000000..d456062bce7
--- /dev/null
+++ b/qa/qa/runtime/rsa_key.rb
@@ -0,0 +1,21 @@
+require 'net/ssh'
+require 'forwardable'
+
+module QA
+ module Runtime
+ class RSAKey
+ extend Forwardable
+
+ attr_reader :key
+ def_delegators :@key, :fingerprint
+
+ def initialize(bits = 4096)
+ @key = OpenSSL::PKey::RSA.new(bits)
+ end
+
+ def public_key
+ @public_key ||= "#{key.ssh_type} #{[key.to_blob].pack('m0')}"
+ end
+ end
+ end
+end
diff --git a/qa/qa/runtime/user.rb b/qa/qa/runtime/user.rb
index 2832439d9e0..60027c89ab1 100644
--- a/qa/qa/runtime/user.rb
+++ b/qa/qa/runtime/user.rb
@@ -10,17 +10,6 @@ module QA
def password
ENV['GITLAB_PASSWORD'] || '5iveL!fe'
end
-
- def ssh_key
- <<~KEY.delete("\n")
- ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFf6RYK3qu/RKF/3ndJmL5xgMLp3O9
- 6x8lTay+QGZ0+9FnnAXMdUqBq/ZU6d/gyMB4IaW3nHzM1w049++yAB6UPCzMB8Uo27K5
- /jyZCtj7Vm9PFNjF/8am1kp46c/SeYicQgQaSBdzIW3UDEa1Ef68qroOlvpi9PYZ/tA7
- M0YP0K5PXX+E36zaIRnJVMPT3f2k+GnrxtjafZrwFdpOP/Fol5BQLBgcsyiU+LM1SuaC
- rzd8c9vyaTA1CxrkxaZh+buAi0PmdDtaDrHd42gqZkXCKavyvgM5o2CkQ5LJHCgzpXy0
- 5qNFzmThBSkb+XtoxbyagBiGbVZtSVow6Xa7qewz= dummy@gitlab.com
- KEY
- end
end
end
end
diff --git a/qa/qa/scenario/entrypoint.rb b/qa/qa/scenario/entrypoint.rb
deleted file mode 100644
index ae099fd911e..00000000000
--- a/qa/qa/scenario/entrypoint.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-module QA
- module Scenario
- ##
- # Base class for running the suite against any GitLab instance,
- # including staging and on-premises installation.
- #
- class Entrypoint < Template
- include Bootable
-
- def perform(address, *files)
- Runtime::Scenario.define(:gitlab_address, address)
-
- ##
- # Perform before hooks, which are different for CE and EE
- #
- Runtime::Release.perform_before_hooks
-
- Specs::Runner.perform do |specs|
- specs.tty = true
- specs.tags = self.class.get_tags
- specs.files = files.any? ? files : 'qa/specs/features'
- end
- end
-
- def self.tags(*tags)
- @tags = tags
- end
-
- def self.get_tags
- @tags
- end
- end
- end
-end
diff --git a/qa/qa/scenario/taggable.rb b/qa/qa/scenario/taggable.rb
new file mode 100644
index 00000000000..b1f24d742e0
--- /dev/null
+++ b/qa/qa/scenario/taggable.rb
@@ -0,0 +1,17 @@
+module QA
+ module Scenario
+ module Taggable
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
+
+ def tags(*tags)
+ @tags = tags
+ end
+
+ def focus
+ @tags.to_a
+ end
+
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
+ end
+ end
+end
diff --git a/qa/qa/scenario/test/instance.rb b/qa/qa/scenario/test/instance.rb
index e2a1f6bf2bd..993bbd723a3 100644
--- a/qa/qa/scenario/test/instance.rb
+++ b/qa/qa/scenario/test/instance.rb
@@ -2,11 +2,29 @@ module QA
module Scenario
module Test
##
- # Run test suite against any GitLab instance,
+ # Base class for running the suite against any GitLab instance,
# including staging and on-premises installation.
#
- class Instance < Entrypoint
+ class Instance < Template
+ include Bootable
+ extend Taggable
+
tags :core
+
+ def perform(address, *files)
+ Runtime::Scenario.define(:gitlab_address, address)
+
+ ##
+ # Perform before hooks, which are different for CE and EE
+ #
+ Runtime::Release.perform_before_hooks
+
+ Specs::Runner.perform do |specs|
+ specs.tty = true
+ specs.tags = self.class.focus
+ specs.files = files.any? ? files : 'qa/specs/features'
+ end
+ end
end
end
end
diff --git a/qa/qa/scenario/test/integration/mattermost.rb b/qa/qa/scenario/test/integration/mattermost.rb
index 7d0702afdb1..d939f52ab16 100644
--- a/qa/qa/scenario/test/integration/mattermost.rb
+++ b/qa/qa/scenario/test/integration/mattermost.rb
@@ -6,7 +6,7 @@ module QA
# Run test suite against any GitLab instance where mattermost is enabled,
# including staging and on-premises installation.
#
- class Mattermost < Scenario::Entrypoint
+ class Mattermost < Test::Instance
tags :core, :mattermost
def perform(address, mattermost, *files)
diff --git a/qa/qa/scenario/test/sanity/selectors.rb b/qa/qa/scenario/test/sanity/selectors.rb
new file mode 100644
index 00000000000..c87eb5f3dfb
--- /dev/null
+++ b/qa/qa/scenario/test/sanity/selectors.rb
@@ -0,0 +1,54 @@
+module QA
+ module Scenario
+ module Test
+ module Sanity
+ class Selectors < Scenario::Template
+ include Scenario::Bootable
+
+ PAGES = [QA::Page].freeze
+
+ def perform(*)
+ validators = PAGES.map do |pages|
+ Page::Validator.new(pages)
+ end
+
+ validators.map(&:errors).flatten.tap do |errors|
+ break if errors.none?
+
+ warn <<~EOS
+ GitLab QA sanity selectors validation test detected problems
+ with your merge request!
+
+ The purpose of this test is to make sure that GitLab QA tests,
+ that are entirely black-box, click-driven scenarios, do match
+ pages structure / layout in GitLab CE / EE repositories.
+
+ It looks like you have changed views / pages / selectors, and
+ these are now out of sync with what we have defined in `qa/`
+ directory.
+
+ Please update the code in `qa/` directory to make it match
+ current changes in this merge request.
+
+ For more help see documentation in `qa/page/README.md` file or
+ ask for help on #qa channel on Slack (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.
+
+ Please see errors described below.
+
+ EOS
+
+ warn errors
+ end
+
+ validators.each(&:validate!)
+
+ puts 'Views / selectors validation passed!'
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/service/omnibus.rb b/qa/qa/service/omnibus.rb
new file mode 100644
index 00000000000..b5c06874e5c
--- /dev/null
+++ b/qa/qa/service/omnibus.rb
@@ -0,0 +1,20 @@
+module QA
+ module Service
+ class Omnibus
+ include Scenario::Actable
+ include Service::Shellout
+
+ def initialize(container)
+ @name = container
+ end
+
+ def gitlab_ctl(command, input: nil)
+ if input.nil?
+ shell "docker exec #{@name} gitlab-ctl #{command}"
+ else
+ shell "docker exec #{@name} bash -c '#{input} | gitlab-ctl #{command}'"
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/service/runner.rb b/qa/qa/service/runner.rb
new file mode 100644
index 00000000000..d0ee33c69f2
--- /dev/null
+++ b/qa/qa/service/runner.rb
@@ -0,0 +1,41 @@
+require 'securerandom'
+
+module QA
+ module Service
+ class Runner
+ include Scenario::Actable
+ include Service::Shellout
+
+ attr_accessor :token, :address, :tags, :image
+
+ def initialize(name)
+ @image = 'gitlab/gitlab-runner:alpine'
+ @name = name || "qa-runner-#{SecureRandom.hex(4)}"
+ @network = Runtime::Scenario.attributes[:network] || 'test'
+ @tags = %w[qa test]
+ end
+
+ def pull
+ shell "docker pull #{@image}"
+ end
+
+ def register!
+ shell <<~CMD.tr("\n", ' ')
+ docker run -d --rm --entrypoint=/bin/sh
+ --network #{@network} --name #{@name}
+ -e CI_SERVER_URL=#{@address}
+ -e REGISTER_NON_INTERACTIVE=true
+ -e REGISTRATION_TOKEN=#{@token}
+ -e RUNNER_EXECUTOR=shell
+ -e RUNNER_TAG_LIST=#{@tags.join(',')}
+ -e RUNNER_NAME=#{@name}
+ #{@image} -c 'gitlab-runner register && gitlab-runner run'
+ CMD
+ end
+
+ def remove!
+ shell "docker rm -f #{@name}"
+ end
+ end
+ end
+end
diff --git a/qa/qa/service/shellout.rb b/qa/qa/service/shellout.rb
new file mode 100644
index 00000000000..898febde63c
--- /dev/null
+++ b/qa/qa/service/shellout.rb
@@ -0,0 +1,23 @@
+require 'open3'
+
+module QA
+ module Service
+ module Shellout
+ ##
+ # TODO, make it possible to use generic QA framework classes
+ # as a library - gitlab-org/gitlab-qa#94
+ #
+ def shell(command)
+ puts "Executing `#{command}`"
+
+ Open3.popen2e(command) do |_in, out, wait|
+ out.each { |line| puts line }
+
+ if wait.value.exited? && wait.value.exitstatus.nonzero?
+ raise "Command `#{command}` failed!"
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/shell/omnibus.rb b/qa/qa/shell/omnibus.rb
deleted file mode 100644
index 6b3628d3109..00000000000
--- a/qa/qa/shell/omnibus.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-require 'open3'
-
-module QA
- module Shell
- class Omnibus
- include Scenario::Actable
-
- def initialize(container)
- @name = container
- end
-
- def gitlab_ctl(command, input: nil)
- if input.nil?
- shell "docker exec #{@name} gitlab-ctl #{command}"
- else
- shell "docker exec #{@name} bash -c '#{input} | gitlab-ctl #{command}'"
- end
- end
-
- private
-
- ##
- # TODO, make it possible to use generic QA framework classes
- # as a library - gitlab-org/gitlab-qa#94
- #
- def shell(command)
- puts "Executing `#{command}`"
-
- Open3.popen2e(command) do |_in, out, wait|
- out.each { |line| puts line }
-
- if wait.value.exited? && wait.value.exitstatus.nonzero?
- raise "Docker command `#{command}` failed!"
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/specs/features/api/users_spec.rb b/qa/qa/specs/features/api/users_spec.rb
new file mode 100644
index 00000000000..9d039590a0e
--- /dev/null
+++ b/qa/qa/specs/features/api/users_spec.rb
@@ -0,0 +1,42 @@
+module QA
+ feature 'API users', :core do
+ before(:context) do
+ @api_client = Runtime::API::Client.new(:gitlab)
+ end
+
+ context 'when authenticated' do
+ let(:request) { Runtime::API::Request.new(@api_client, '/users') }
+
+ scenario 'get list of users' do
+ get request.url
+
+ expect_status(200)
+ end
+
+ scenario 'submit request with a valid user name' do
+ get request.url, { params: { username: 'root' } }
+
+ expect_status(200)
+ expect(json_body).to be_an Array
+ expect(json_body.size).to eq(1)
+ expect(json_body.first[:username]).to eq Runtime::User.name
+ end
+
+ scenario 'submit request with an invalid user name' do
+ get request.url, { params: { username: 'invalid' } }
+
+ expect_status(200)
+ expect(json_body).to be_an Array
+ expect(json_body.size).to eq(0)
+ end
+ end
+
+ scenario 'submit request with an invalid token' do
+ request = Runtime::API::Request.new(@api_client, '/users', personal_access_token: 'invalid')
+
+ get request.url
+
+ expect_status(401)
+ end
+ end
+end
diff --git a/qa/qa/specs/features/merge_request/create_spec.rb b/qa/qa/specs/features/merge_request/create_spec.rb
new file mode 100644
index 00000000000..fbf9a4d17e5
--- /dev/null
+++ b/qa/qa/specs/features/merge_request/create_spec.rb
@@ -0,0 +1,17 @@
+module QA
+ feature 'creates a merge request', :core do
+ scenario 'user creates a new merge request' do
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.act { sign_in_using_credentials }
+
+ Factory::Resource::MergeRequest.fabricate! do |merge_request|
+ merge_request.title = 'This is a merge request'
+ merge_request.description = 'Great feature'
+ end
+
+ expect(page).to have_content('This is a merge request')
+ expect(page).to have_content('Great feature')
+ expect(page).to have_content('Opened less than a minute ago')
+ end
+ end
+end
diff --git a/qa/qa/specs/features/project/activity_spec.rb b/qa/qa/specs/features/project/activity_spec.rb
new file mode 100644
index 00000000000..ba94ce8cf28
--- /dev/null
+++ b/qa/qa/specs/features/project/activity_spec.rb
@@ -0,0 +1,20 @@
+module QA
+ feature 'activity page', :core do
+ scenario 'push creates an event in the activity page' do
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.act { sign_in_using_credentials }
+
+ Factory::Repository::Push.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::Activity.act { go_to_push_events }
+
+ expect(page).to have_content('pushed new branch master')
+ end
+ end
+end
diff --git a/qa/qa/specs/features/project/add_deploy_key_spec.rb b/qa/qa/specs/features/project/add_deploy_key_spec.rb
index 43a85213501..b9998dda895 100644
--- a/qa/qa/specs/features/project/add_deploy_key_spec.rb
+++ b/qa/qa/specs/features/project/add_deploy_key_spec.rb
@@ -1,22 +1,20 @@
module QA
feature 'deploy keys support', :core do
- given(:deploy_key_title) { 'deploy key title' }
- given(:deploy_key_value) { Runtime::User.ssh_key }
-
scenario 'user adds a deploy key' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- Factory::Resource::DeployKey.fabricate! do |deploy_key|
- deploy_key.title = deploy_key_title
- deploy_key.key = deploy_key_value
- end
+ key = Runtime::RSAKey.new
+ deploy_key_title = 'deploy key title'
+ deploy_key_value = key.public_key
- Page::Project::Settings::Repository.perform do |setting|
- setting.expand_deploy_keys do |page|
- expect(page).to have_key_title(deploy_key_title)
- end
+ deploy_key = Factory::Resource::DeployKey.fabricate! do |resource|
+ resource.title = deploy_key_title
+ resource.key = deploy_key_value
end
+
+ expect(deploy_key.title).to eq(deploy_key_title)
+ expect(deploy_key.fingerprint).to eq(key.fingerprint)
end
end
end
diff --git a/qa/qa/specs/features/project/add_secret_variable_spec.rb b/qa/qa/specs/features/project/add_secret_variable_spec.rb
new file mode 100644
index 00000000000..36422a92afc
--- /dev/null
+++ b/qa/qa/specs/features/project/add_secret_variable_spec.rb
@@ -0,0 +1,19 @@
+module QA
+ feature 'secret variables support', :core do
+ scenario 'user adds a secret variable' do
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.act { sign_in_using_credentials }
+
+ variable_key = 'VARIABLE_KEY'
+ variable_value = 'variable value'
+
+ variable = Factory::Resource::SecretVariable.fabricate! do |resource|
+ resource.key = variable_key
+ resource.value = variable_value
+ end
+
+ expect(variable.key).to eq(variable_key)
+ expect(variable.value).to eq(variable_value)
+ end
+ end
+end
diff --git a/qa/qa/specs/features/project/create_issue_spec.rb b/qa/qa/specs/features/project/create_issue_spec.rb
new file mode 100644
index 00000000000..b73f108c2d9
--- /dev/null
+++ b/qa/qa/specs/features/project/create_issue_spec.rb
@@ -0,0 +1,18 @@
+module QA
+ feature 'creates issue', :core do
+ let(:issue_title) { 'issue title' }
+
+ scenario 'user creates issue' do
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.act { sign_in_using_credentials }
+
+ Factory::Resource::Issue.fabricate! do |issue|
+ issue.title = issue_title
+ end
+
+ Page::Menu::Side.act { click_issues }
+
+ expect(page).to have_content(issue_title)
+ end
+ end
+end
diff --git a/qa/qa/specs/features/project/create_spec.rb b/qa/qa/specs/features/project/create_spec.rb
index 61c19378ae0..b1c07249892 100644
--- a/qa/qa/specs/features/project/create_spec.rb
+++ b/qa/qa/specs/features/project/create_spec.rb
@@ -4,11 +4,13 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- Factory::Resource::Project.fabricate! do |project|
+ created_project = Factory::Resource::Project.fabricate! 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(
/Project \S?awesome-project\S+ was successfully created/
)
diff --git a/qa/qa/specs/features/project/pipelines_spec.rb b/qa/qa/specs/features/project/pipelines_spec.rb
new file mode 100644
index 00000000000..1bb7730e06c
--- /dev/null
+++ b/qa/qa/specs/features/project/pipelines_spec.rb
@@ -0,0 +1,102 @@
+module QA
+ feature 'CI/CD Pipelines', :core, :docker do
+ let(:executor) { "qa-runner-#{Time.now.to_i}" }
+
+ after do
+ Service::Runner.new(executor).remove!
+ end
+
+ scenario 'user registers a new specific runner' do
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.act { sign_in_using_credentials }
+
+ Factory::Resource::Runner.fabricate! do |runner|
+ runner.name = executor
+ end
+
+ Page::Project::Settings::CICD.perform do |settings|
+ sleep 5 # Runner should register within 5 seconds
+ settings.refresh
+
+ settings.expand_runners_settings do |page|
+ expect(page).to have_content(executor)
+ expect(page).to have_online_runner
+ end
+ end
+ end
+
+ scenario 'users creates a new pipeline' do
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.act { sign_in_using_credentials }
+
+ project = Factory::Resource::Project.fabricate! do |project|
+ project.name = 'project-with-pipelines'
+ project.description = 'Project with CI/CD Pipelines.'
+ end
+
+ Factory::Resource::Runner.fabricate! do |runner|
+ runner.project = project
+ runner.name = executor
+ runner.tags = %w[qa test]
+ end
+
+ Factory::Repository::Push.fabricate! do |push|
+ push.project = project
+ push.file_name = '.gitlab-ci.yml'
+ push.commit_message = 'Add .gitlab-ci.yml'
+ push.file_content = <<~EOF
+ test-success:
+ tags:
+ - qa
+ - test
+ script: echo 'OK'
+
+ test-failure:
+ tags:
+ - qa
+ - test
+ script:
+ - echo 'FAILURE'
+ - exit 1
+
+ test-tags:
+ tags:
+ - qa
+ - docker
+ script: echo 'NOOP'
+
+ test-artifacts:
+ tags:
+ - qa
+ - test
+ script: echo "CONTENTS" > my-artifacts/artifact.txt
+ artifacts:
+ paths:
+ - my-artifacts/
+ EOF
+ end
+
+ Page::Project::Show.act { wait_for_push }
+
+ expect(page).to have_content('Add .gitlab-ci.yml')
+
+ Page::Menu::Side.act { click_ci_cd_pipelines }
+
+ expect(page).to have_content('All 1')
+ expect(page).to have_content('Add .gitlab-ci.yml')
+
+ puts 'Waiting for the runner to process the pipeline'
+ sleep 15 # Runner should process all jobs within 15 seconds.
+
+ Page::Project::Pipeline::Index.act { go_to_latest_pipeline }
+
+ Page::Project::Pipeline::Show.perform do |pipeline|
+ expect(pipeline).to be_running
+ expect(pipeline).to have_build('test-success', status: :success)
+ expect(pipeline).to have_build('test-failure', status: :failed)
+ expect(pipeline).to have_build('test-tags', status: :pending)
+ expect(pipeline).to have_build('test-artifacts', status: :failed)
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/repository/push_spec.rb b/qa/qa/specs/features/repository/push_spec.rb
index 4f6ffe14c9f..51d9c2c7fd2 100644
--- a/qa/qa/specs/features/repository/push_spec.rb
+++ b/qa/qa/specs/features/repository/push_spec.rb
@@ -11,10 +11,7 @@ module QA
push.commit_message = 'Add README.md'
end
- Page::Project::Show.act do
- wait_for_push
- refresh
- end
+ Page::Project::Show.act { wait_for_push }
expect(page).to have_content('README.md')
expect(page).to have_content('This is a test project')
diff --git a/qa/spec/factory/base_spec.rb b/qa/spec/factory/base_spec.rb
index a3ba0176819..c5663049be8 100644
--- a/qa/spec/factory/base_spec.rb
+++ b/qa/spec/factory/base_spec.rb
@@ -1,8 +1,9 @@
describe QA::Factory::Base do
+ let(:factory) { spy('factory') }
+ let(:product) { spy('product') }
+
describe '.fabricate!' do
subject { Class.new(described_class) }
- let(:factory) { spy('factory') }
- let(:product) { spy('product') }
before do
allow(QA::Factory::Product).to receive(:new).and_return(product)
@@ -18,7 +19,6 @@ describe QA::Factory::Base do
it 'returns fabrication product' do
allow(subject).to receive(:new).and_return(factory)
- allow(factory).to receive(:fabricate!).and_return('something')
result = subject.fabricate!('something')
@@ -59,30 +59,63 @@ describe QA::Factory::Base do
it 'defines dependency accessors' do
expect(subject.new).to respond_to :mydep, :mydep=
end
- end
- describe 'building dependencies' do
- let(:dependency) { double('dependency') }
- let(:instance) { spy('instance') }
+ 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)
+ 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
- dependency Some::MyDependency, as: :mydep
+ product :token do
+ page.do_something_on_page!
+ 'resulting value'
+ end
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)
+ it 'appends new product attribute' do
+ expect(subject.attributes).to be_one
+ expect(subject.attributes).to have_key(:token)
end
- it 'builds all dependencies first' do
- expect(dependency).to receive(:fabricate!).once
+ describe 'populating fabrication product with data' do
+ let(:page) { spy('page') }
+
+ before do
+ allow(subject).to receive(:new).and_return(factory)
+ allow(QA::Factory::Product).to receive(:new).and_return(product)
+ allow(product).to receive(:page).and_return(page)
+ end
- subject.fabricate!
+ it 'populates product after fabrication' do
+ subject.fabricate!
+
+ expect(page).to have_received(:do_something_on_page!)
+ expect(product.token).to eq 'resulting value'
+ end
end
end
end
diff --git a/qa/spec/factory/dependency_spec.rb b/qa/spec/factory/dependency_spec.rb
index 32405415126..8aaa6665a18 100644
--- a/qa/spec/factory/dependency_spec.rb
+++ b/qa/spec/factory/dependency_spec.rb
@@ -54,6 +54,19 @@ describe QA::Factory::Dependency do
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
index 3d9e86a641b..fdfb1ec90cc 100644
--- a/qa/spec/factory/product_spec.rb
+++ b/qa/spec/factory/product_spec.rb
@@ -3,19 +3,8 @@ describe QA::Factory::Product do
let(:product) { spy('product') }
describe '.populate!' do
- it 'instantiates and yields factory' do
- expect(described_class).to receive(:new).with(factory)
-
- described_class.populate!(factory) do |instance|
- instance.something = 'string'
- end
-
- expect(factory).to have_received(:something=).with('string')
- end
-
it 'returns a fabrication product' do
- expect(described_class).to receive(:new)
- .with(factory).and_return(product)
+ expect(described_class).to receive(:new).and_return(product)
result = described_class.populate!(factory) do |instance|
instance.something = 'string'
@@ -23,11 +12,6 @@ describe QA::Factory::Product do
expect(result).to be product
end
-
- it 'raises unless block given' do
- expect { described_class.populate!(factory) }
- .to raise_error ArgumentError
- end
end
describe '.visit!' do
@@ -37,8 +21,7 @@ describe QA::Factory::Product do
allow_any_instance_of(described_class)
.to receive(:visit).and_return('visited some url')
- expect(described_class.new(factory).visit!)
- .to eq 'visited some url'
+ expect(subject.visit!).to eq 'visited some url'
end
end
end
diff --git a/qa/spec/fixtures/banana_sample.gif b/qa/spec/fixtures/banana_sample.gif
new file mode 100644
index 00000000000..1322ac92d14
--- /dev/null
+++ b/qa/spec/fixtures/banana_sample.gif
Binary files differ
diff --git a/qa/spec/page/base_spec.rb b/qa/spec/page/base_spec.rb
new file mode 100644
index 00000000000..287adf35c46
--- /dev/null
+++ b/qa/spec/page/base_spec.rb
@@ -0,0 +1,63 @@
+describe QA::Page::Base do
+ describe 'page helpers' do
+ it 'exposes helpful page helpers' do
+ expect(subject).to respond_to :refresh, :wait, :scroll_to
+ end
+ end
+
+ describe '.view', 'DSL for defining view partials' 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/
+ end
+
+ view 'path/to/some/_partial.html.haml' do
+ element :something, 'string pattern'
+ end
+ end
+ end
+
+ it 'makes it possible to define page views' do
+ expect(subject.views.size).to eq 2
+ expect(subject.views).to all(be_an_instance_of QA::Page::View)
+ end
+
+ it 'populates views objects with data about elements' do
+ subject.views.first.elements.tap do |elements|
+ expect(elements.size).to eq 2
+ expect(elements).to all(be_an_instance_of QA::Page::Element)
+ expect(elements.map(&:name)).to eq [:something, :something_else]
+ end
+ end
+ end
+
+ describe '.errors' do
+ let(:view) { double('view') }
+
+ context 'when page has views and elements defined' do
+ before do
+ allow(described_class).to receive(:views)
+ .and_return([view])
+
+ allow(view).to receive(:errors).and_return(['some error'])
+ end
+
+ it 'iterates views composite and returns errors' do
+ expect(described_class.errors).to eq ['some error']
+ end
+ end
+
+ context 'when page has no views and elements defined' do
+ before do
+ allow(described_class).to receive(:views).and_return([])
+ end
+
+ it 'appends an error about missing views / elements block' do
+ expect(described_class.errors)
+ .to include 'Page class does not have views / elements defined!'
+ end
+ end
+ end
+end
diff --git a/qa/spec/page/element_spec.rb b/qa/spec/page/element_spec.rb
new file mode 100644
index 00000000000..8598c57ad34
--- /dev/null
+++ b/qa/spec/page/element_spec.rb
@@ -0,0 +1,51 @@
+describe QA::Page::Element do
+ describe '#selector' do
+ it 'transforms element name into QA-specific selector' do
+ expect(described_class.new(:sign_in_button).selector)
+ .to eq 'qa-sign-in-button'
+ end
+ end
+
+ describe '#selector_css' do
+ it 'transforms element name into QA-specific clickable css selector' do
+ expect(described_class.new(:sign_in_button).selector_css)
+ .to eq '.qa-sign-in-button'
+ end
+ end
+
+ context 'when pattern is an expression' do
+ subject { described_class.new(:something, /button 'Sign in'/) }
+
+ it 'matches when there is a match' do
+ expect(subject.matches?("button 'Sign in'")).to be true
+ end
+
+ it 'does not match if pattern is not present' do
+ expect(subject.matches?("button 'Sign out'")).to be false
+ end
+ end
+
+ context 'when pattern is a string' do
+ subject { described_class.new(:something, 'button') }
+
+ it 'matches when there is match' do
+ expect(subject.matches?('some button in the view')).to be true
+ end
+
+ it 'does not match if pattern is not present' do
+ expect(subject.matches?('text_field :name')).to be false
+ end
+ end
+
+ context 'when pattern is not provided' do
+ subject { described_class.new(:some_name) }
+
+ it 'matches when QA specific selector is present' do
+ expect(subject.matches?('some qa-some-name selector')).to be true
+ end
+
+ it 'does not match if QA selector is not there' do
+ expect(subject.matches?('some_name selector')).to be false
+ end
+ end
+end
diff --git a/qa/spec/page/validator_spec.rb b/qa/spec/page/validator_spec.rb
new file mode 100644
index 00000000000..02822d7d18f
--- /dev/null
+++ b/qa/spec/page/validator_spec.rb
@@ -0,0 +1,79 @@
+describe QA::Page::Validator do
+ describe '#constants' do
+ subject do
+ described_class.new(QA::Page::Project)
+ end
+
+ it 'returns all constants that are module children' do
+ expect(subject.constants)
+ .to include QA::Page::Project::New, QA::Page::Project::Settings
+ end
+ end
+
+ describe '#descendants' do
+ subject do
+ described_class.new(QA::Page::Project)
+ end
+
+ it 'recursively returns all descendants that are page objects' do
+ expect(subject.descendants)
+ .to include QA::Page::Project::New, QA::Page::Project::Settings::Repository
+ end
+
+ it 'does not return modules that aggregate page objects' do
+ expect(subject.descendants)
+ .not_to include QA::Page::Project::Settings
+ end
+ end
+
+ context 'when checking validation errors' do
+ let(:view) { spy('view') }
+
+ before do
+ allow(QA::Page::Admin::Settings)
+ .to receive(:views).and_return([view])
+ end
+
+ subject do
+ described_class.new(QA::Page::Admin)
+ end
+
+ context 'when there are no validation errors' do
+ before do
+ allow(view).to receive(:errors).and_return([])
+ end
+
+ describe '#errors' do
+ it 'does not return errors' do
+ expect(subject.errors).to be_empty
+ end
+ end
+
+ describe '#validate!' do
+ it 'does not raise error' do
+ expect { subject.validate! }.not_to raise_error
+ end
+ end
+ end
+
+ context 'when there are validation errors' do
+ before do
+ allow(view).to receive(:errors)
+ .and_return(['some error', 'another error'])
+ end
+
+ describe '#errors' do
+ it 'returns errors' do
+ expect(subject.errors.count).to eq 2
+ end
+ end
+
+ describe '#validate!' do
+ it 'raises validation error' do
+ expect { subject.validate! }
+ .to raise_error described_class::ValidationError
+ end
+ end
+ end
+ end
+end
diff --git a/qa/spec/page/view_spec.rb b/qa/spec/page/view_spec.rb
new file mode 100644
index 00000000000..aedbc3863a7
--- /dev/null
+++ b/qa/spec/page/view_spec.rb
@@ -0,0 +1,70 @@
+describe QA::Page::View do
+ let(:element) do
+ double('element', name: :something, pattern: /some element/)
+ end
+
+ subject { described_class.new('some/file.html', [element]) }
+
+ 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/
+ end
+
+ expect(results.elements.size).to eq 2
+ end
+ end
+
+ describe '#pathname' do
+ it 'returns an absolute and clean path to the view' do
+ expect(subject.pathname.to_s).not_to include 'qa/page/'
+ expect(subject.pathname.to_s).to include 'some/file.html'
+ end
+ end
+
+ describe '#errors' do
+ context 'when view partial is present' do
+ before do
+ allow(subject.pathname).to receive(:readable?)
+ .and_return(true)
+ end
+
+ context 'when pattern is found' do
+ before do
+ allow(File).to receive(:foreach)
+ .and_yield('some element').once
+ allow(element).to receive(:matches?)
+ .with('some element').and_return(true)
+ end
+
+ it 'walks through the view and asserts on elements existence' do
+ expect(subject.errors).to be_empty
+ end
+ end
+
+ context 'when pattern has not been found' do
+ before do
+ allow(File).to receive(:foreach)
+ .and_yield('some element').once
+ allow(element).to receive(:matches?)
+ .with('some element').and_return(false)
+ end
+
+ it 'returns an array of errors related to missing elements' do
+ expect(subject.errors).not_to be_empty
+ expect(subject.errors.first)
+ .to match %r(Missing element `.*` in `.*/some/file.html` view)
+ end
+ end
+ end
+
+ context 'when view partial has not been found' do
+ it 'returns an error when it is not able to find the partial' do
+ expect(subject.errors).to be_one
+ expect(subject.errors.first)
+ .to match %r(Missing view partial `.*/some/file.html`!)
+ end
+ end
+ end
+end
diff --git a/qa/spec/runtime/api_client_spec.rb b/qa/spec/runtime/api_client_spec.rb
new file mode 100644
index 00000000000..d497d8839b8
--- /dev/null
+++ b/qa/spec/runtime/api_client_spec.rb
@@ -0,0 +1,30 @@
+describe QA::Runtime::API::Client do
+ include Support::StubENV
+
+ describe 'initialization' do
+ it 'defaults to :gitlab address' do
+ expect(described_class.new.address).to eq :gitlab
+ end
+
+ it 'uses specified address' do
+ client = described_class.new('http:///example.com')
+
+ expect(client.address).to eq 'http:///example.com'
+ end
+ end
+
+ describe '#get_personal_access_token' do
+ it 'returns specified token from env' do
+ stub_env('PERSONAL_ACCESS_TOKEN', 'a_token')
+
+ expect(described_class.new.get_personal_access_token).to eq 'a_token'
+ end
+
+ it 'returns a created token' do
+ allow_any_instance_of(described_class)
+ .to receive(:create_personal_access_token).and_return('created_token')
+
+ expect(described_class.new.get_personal_access_token).to eq 'created_token'
+ end
+ end
+end
diff --git a/qa/spec/runtime/api_request_spec.rb b/qa/spec/runtime/api_request_spec.rb
new file mode 100644
index 00000000000..9a1ed8a7a46
--- /dev/null
+++ b/qa/spec/runtime/api_request_spec.rb
@@ -0,0 +1,42 @@
+describe QA::Runtime::API::Request do
+ include Support::StubENV
+
+ before do
+ stub_env('PERSONAL_ACCESS_TOKEN', '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
+ expect(request.url).to eq 'http://example.com/api/v4/users?private_token=a_token'
+ end
+ end
+
+ describe '#request_path' do
+ it 'prepends the api path' do
+ expect(request.request_path('/users')).to eq '/api/v4/users'
+ end
+
+ it 'adds the personal access token' do
+ expect(request.request_path('/users', personal_access_token: 'token'))
+ .to eq '/api/v4/users?private_token=token'
+ end
+
+ it 'adds the oauth access token' do
+ expect(request.request_path('/users', oauth_access_token: 'otoken'))
+ .to eq '/api/v4/users?access_token=otoken'
+ end
+
+ it 'respects query parameters' do
+ expect(request.request_path('/users?page=1')).to eq '/api/v4/users?page=1'
+ expect(request.request_path('/users?page=1', personal_access_token: 'token'))
+ .to eq '/api/v4/users?page=1&private_token=token'
+ end
+
+ it 'uses a different api version' do
+ expect(request.request_path('/users', version: 'v3')).to eq '/api/v3/users'
+ end
+ end
+end
diff --git a/qa/spec/runtime/env_spec.rb b/qa/spec/runtime/env_spec.rb
index 57a72a04507..103573db6be 100644
--- a/qa/spec/runtime/env_spec.rb
+++ b/qa/spec/runtime/env_spec.rb
@@ -1,7 +1,5 @@
describe QA::Runtime::Env do
- before do
- allow(ENV).to receive(:[]).and_call_original
- end
+ include Support::StubENV
describe '.chrome_headless?' do
context 'when there is an env variable set' do
@@ -57,8 +55,4 @@ describe QA::Runtime::Env do
end
end
end
-
- def stub_env(name, value)
- allow(ENV).to receive(:[]).with(name).and_return(value)
- end
end
diff --git a/qa/spec/runtime/rsa_key.rb b/qa/spec/runtime/rsa_key.rb
new file mode 100644
index 00000000000..6d7ab4dcd2e
--- /dev/null
+++ b/qa/spec/runtime/rsa_key.rb
@@ -0,0 +1,9 @@
+describe QA::Runtime::RSAKey do
+ describe '#public_key' do
+ subject { described_class.new.public_key }
+
+ it 'generates a public RSA key' do
+ expect(subject).to match(%r{\Assh\-rsa AAAA[0-9A-Za-z+/]+={0,3}\z})
+ end
+ end
+end
diff --git a/qa/spec/scenario/entrypoint_spec.rb b/qa/spec/scenario/test/instance_spec.rb
index aec79dcea04..1824db54c9b 100644
--- a/qa/spec/scenario/entrypoint_spec.rb
+++ b/qa/spec/scenario/test/instance_spec.rb
@@ -1,6 +1,6 @@
-describe QA::Scenario::Entrypoint do
+describe QA::Scenario::Test::Instance do
subject do
- Class.new(QA::Scenario::Entrypoint) do
+ Class.new(described_class) do
tags :rspec
end
end
diff --git a/qa/spec/scenario/test/sanity/selectors_spec.rb b/qa/spec/scenario/test/sanity/selectors_spec.rb
new file mode 100644
index 00000000000..45d21d54955
--- /dev/null
+++ b/qa/spec/scenario/test/sanity/selectors_spec.rb
@@ -0,0 +1,40 @@
+describe QA::Scenario::Test::Sanity::Selectors do
+ let(:validator) { spy('validator') }
+
+ before do
+ stub_const('QA::Page::Validator', validator)
+ end
+
+ context 'when there are errors detected' do
+ before do
+ allow(validator).to receive(:errors).and_return(['some error'])
+ end
+
+ it 'outputs information about errors' do
+ expect { described_class.perform }
+ .to output(/some error/).to_stderr
+
+ expect { described_class.perform }
+ .to output(/electors validation test detected problems/)
+ .to_stderr
+ end
+ end
+
+ context 'when there are no errors detected' do
+ before do
+ allow(validator).to receive(:errors).and_return([])
+ end
+
+ it 'processes pages module' do
+ described_class.perform
+
+ expect(validator).to have_received(:new).with(QA::Page)
+ end
+
+ it 'triggers validation' do
+ described_class.perform
+
+ expect(validator).to have_received(:validate!).at_least(:once)
+ end
+ end
+end
diff --git a/qa/spec/spec_helper.rb b/qa/spec/spec_helper.rb
index 64d06ef6558..c2c6cf95406 100644
--- a/qa/spec/spec_helper.rb
+++ b/qa/spec/spec_helper.rb
@@ -1,5 +1,7 @@
require_relative '../qa'
+Dir[File.join(__dir__, 'support', '**', '*.rb')].each { |f| require f }
+
RSpec.configure do |config|
config.expect_with :rspec do |expectations|
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
diff --git a/qa/spec/support/stub_env.rb b/qa/spec/support/stub_env.rb
new file mode 100644
index 00000000000..bc8f3a5e22e
--- /dev/null
+++ b/qa/spec/support/stub_env.rb
@@ -0,0 +1,38 @@
+# Inspired by https://github.com/ljkbennett/stub_env/blob/master/lib/stub_env/helpers.rb
+module Support
+ module StubENV
+ def stub_env(key_or_hash, value = nil)
+ init_stub unless env_stubbed?
+
+ if key_or_hash.is_a? Hash
+ key_or_hash.each { |k, v| add_stubbed_value(k, v) }
+ else
+ add_stubbed_value key_or_hash, value
+ end
+ end
+
+ private
+
+ STUBBED_KEY = '__STUBBED__'.freeze
+
+ def add_stubbed_value(key, value)
+ allow(ENV).to receive(:[]).with(key).and_return(value)
+ allow(ENV).to receive(:key?).with(key).and_return(true)
+ allow(ENV).to receive(:fetch).with(key).and_return(value)
+ allow(ENV).to receive(:fetch).with(key, anything()) do |_, default_val|
+ value || default_val
+ end
+ end
+
+ def env_stubbed?
+ ENV[STUBBED_KEY]
+ end
+
+ def init_stub
+ allow(ENV).to receive(:[]).and_call_original
+ allow(ENV).to receive(:key?).and_call_original
+ allow(ENV).to receive(:fetch).and_call_original
+ add_stubbed_value(STUBBED_KEY, true)
+ end
+ end
+end