summaryrefslogtreecommitdiff
path: root/qa
diff options
context:
space:
mode:
authorLin Jen-Shin <godfat@godfat.org>2018-01-12 19:43:38 +0800
committerLin Jen-Shin <godfat@godfat.org>2018-01-12 19:43:38 +0800
commitcfd75101d19db3235b64b05d7a58616db40f22c6 (patch)
tree6eef1dd8bd6a1ddc8f69ffdf35bf2f9b6771c30c /qa
parentf4bd9c0b5e1eafe6de855d73bfb606909229f382 (diff)
parentf9579df8617add53424f57c0feedfa601a77e923 (diff)
downloadgitlab-ce-cfd75101d19db3235b64b05d7a58616db40f22c6.tar.gz
Merge remote-tracking branch 'upstream/master' into 1819-override-ce
* upstream/master: (621 commits) Add a note about GitLab QA page objects validator to docs Refactor dispatcher projects blame and blob path Update export message to mention we can download the file from the UI Fix Ctrl+Enter keyboard shortcut saving comment/note edit fix case where tooltip messes up :last-child selector Add reason to keep postgresql 9.2 for CI Remove warning noise in ProjectImportOptions Add changelog entry Add RedirectRoute factory Update Ingress extra cost note to be more generic Fix Rubocop offense Refactor dispatcher project branches path Revert "Revert "Fix Route validation for unchanged path"" Document that we need rsync for backing up Docs: move article "Laravel and Envoy w/ CI/CD" Recommend against the use of EFS Adds Rubocop rule for line break around conditionals Update CHANGELOG.md for 10.1.6 Filter out build traces from logged parameters Refactored project:n* imports in dispatcher.js ...
Diffstat (limited to 'qa')
-rw-r--r--qa/README.md19
-rw-r--r--qa/qa.rb23
-rw-r--r--qa/qa/factory/base.rb41
-rw-r--r--qa/qa/factory/product.rb17
-rw-r--r--qa/qa/factory/resource/deploy_key.rb31
-rw-r--r--qa/qa/factory/resource/project.rb4
-rw-r--r--qa/qa/factory/resource/sandbox.rb2
-rw-r--r--qa/qa/factory/settings/hashed_storage.rb6
-rw-r--r--qa/qa/page/README.md112
-rw-r--r--qa/qa/page/admin/menu.rb15
-rw-r--r--qa/qa/page/admin/settings.rb7
-rw-r--r--qa/qa/page/base.rb33
-rw-r--r--qa/qa/page/dashboard/groups.rb9
-rw-r--r--qa/qa/page/dashboard/projects.rb2
-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.rb7
-rw-r--r--qa/qa/page/main/login.rb12
-rw-r--r--qa/qa/page/main/menu.rb50
-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.rb22
-rw-r--r--qa/qa/page/menu/main.rb71
-rw-r--r--qa/qa/page/menu/side.rb35
-rw-r--r--qa/qa/page/project/new.rb13
-rw-r--r--qa/qa/page/project/settings/common.rb17
-rw-r--r--qa/qa/page/project/settings/deploy_keys.rb34
-rw-r--r--qa/qa/page/project/settings/repository.rb24
-rw-r--r--qa/qa/page/project/show.rb17
-rw-r--r--qa/qa/page/validator.rb52
-rw-r--r--qa/qa/page/view.rb55
-rw-r--r--qa/qa/runtime/browser.rb41
-rw-r--r--qa/qa/runtime/env.rb15
-rw-r--r--qa/qa/runtime/namespace.rb4
-rw-r--r--qa/qa/runtime/user.rb11
-rw-r--r--qa/qa/scenario/test/sanity/selectors.rb54
-rw-r--r--qa/qa/specs/features/login/standard_spec.rb2
-rw-r--r--qa/qa/specs/features/mattermost/group_create_spec.rb2
-rw-r--r--qa/qa/specs/features/project/add_deploy_key_spec.rb22
-rw-r--r--qa/qa/specs/features/project/create_spec.rb4
-rw-r--r--qa/spec/factory/base_spec.rb66
-rw-r--r--qa/spec/factory/product_spec.rb21
-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/env_spec.rb64
-rw-r--r--qa/spec/scenario/test/sanity/selectors_spec.rb40
49 files changed, 1263 insertions, 137 deletions
diff --git a/qa/README.md b/qa/README.md
index 7f2dd39ff63..3c1b61900d9 100644
--- a/qa/README.md
+++ b/qa/README.md
@@ -17,6 +17,17 @@ 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
@@ -27,13 +38,17 @@ following call would login to a local [GDK] instance and run all specs in
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 340f5e35c67..4803432aeee 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -10,6 +10,7 @@ module QA
autoload :Namespace, 'qa/runtime/namespace'
autoload :Scenario, 'qa/runtime/scenario'
autoload :Browser, 'qa/runtime/browser'
+ autoload :Env, 'qa/runtime/env'
end
##
@@ -24,6 +25,7 @@ module QA
autoload :Sandbox, 'qa/factory/resource/sandbox'
autoload :Group, 'qa/factory/resource/group'
autoload :Project, 'qa/factory/resource/project'
+ autoload :DeployKey, 'qa/factory/resource/deploy_key'
end
module Repository
@@ -56,6 +58,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
@@ -66,13 +72,21 @@ 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'
- autoload :Menu, 'qa/page/main/menu'
autoload :OAuth, 'qa/page/main/oauth'
end
+ module Menu
+ autoload :Main, 'qa/page/menu/main'
+ autoload :Side, 'qa/page/menu/side'
+ autoload :Admin, 'qa/page/menu/admin'
+ end
+
module Dashboard
autoload :Projects, 'qa/page/dashboard/projects'
autoload :Groups, 'qa/page/dashboard/groups'
@@ -86,10 +100,15 @@ module QA
module Project
autoload :New, 'qa/page/project/new'
autoload :Show, 'qa/page/project/show'
+
+ module Settings
+ autoload :Common, 'qa/page/project/settings/common'
+ autoload :Repository, 'qa/page/project/settings/repository'
+ autoload :DeployKeys, 'qa/page/project/settings/deploy_keys'
+ end
end
module Admin
- autoload :Menu, 'qa/page/admin/menu'
autoload :Settings, 'qa/page/admin/settings'
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/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
new file mode 100644
index 00000000000..7c58e70bcc4
--- /dev/null
+++ b/qa/qa/factory/resource/deploy_key.rb
@@ -0,0 +1,31 @@
+module QA
+ module Factory
+ module Resource
+ class DeployKey < Factory::Base
+ attr_accessor :title, :key
+
+ dependency Factory::Resource::Project, as: :project do |project|
+ project.name = 'project-to-deploy'
+ project.description = 'project for adding deploy key test'
+ end
+
+ def fabricate!
+ project.visit!
+
+ Page::Menu::Side.act do
+ click_repository_setting
+ end
+
+ Page::Project::Settings::Repository.perform do |setting|
+ setting.expand_deploy_keys do |page|
+ page.fill_key_title(title)
+ page.fill_key_value(key)
+
+ page.add_key
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/factory/resource/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/sandbox.rb b/qa/qa/factory/resource/sandbox.rb
index 558da1c973b..ad376988e82 100644
--- a/qa/qa/factory/resource/sandbox.rb
+++ b/qa/qa/factory/resource/sandbox.rb
@@ -11,7 +11,7 @@ module QA
end
def fabricate!
- Page::Main::Menu.act { go_to_groups }
+ Page::Menu::Main.act { go_to_groups }
Page::Dashboard::Groups.perform do |page|
if page.has_group?(@name)
diff --git a/qa/qa/factory/settings/hashed_storage.rb b/qa/qa/factory/settings/hashed_storage.rb
index eb3b28f2613..13ce2435fe4 100644
--- a/qa/qa/factory/settings/hashed_storage.rb
+++ b/qa/qa/factory/settings/hashed_storage.rb
@@ -6,15 +6,15 @@ module QA
raise ArgumentError unless traits.include?(:enabled)
Page::Main::Login.act { sign_in_using_credentials }
- Page::Main::Menu.act { go_to_admin_area }
- Page::Admin::Menu.act { go_to_settings }
+ Page::Menu::Main.act { go_to_admin_area }
+ Page::Menu::Admin.act { go_to_settings }
Page::Admin::Settings.act do
enable_hashed_storage
save_settings
end
- QA::Page::Main::Menu.act { sign_out }
+ QA::Page::Menu::Main.act { sign_out }
end
end
end
diff --git a/qa/qa/page/README.md b/qa/qa/page/README.md
new file mode 100644
index 00000000000..f72fbfeafca
--- /dev/null
+++ b/qa/qa/page/README.md
@@ -0,0 +1,112 @@
+# 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 :passowrd_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
+```
+
+## 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/menu.rb b/qa/qa/page/admin/menu.rb
deleted file mode 100644
index dd289ffe269..00000000000
--- a/qa/qa/page/admin/menu.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-module QA
- module Page
- module Admin
- class Menu < Page::Base
- def go_to_license
- click_link 'License'
- end
-
- def go_to_settings
- click_link 'Settings'
- end
- end
- end
- end
-end
diff --git a/qa/qa/page/admin/settings.rb b/qa/qa/page/admin/settings.rb
index 39e2f2062ad..1904732aee6 100644
--- a/qa/qa/page/admin/settings.rb
+++ b/qa/qa/page/admin/settings.rb
@@ -2,6 +2,13 @@ module QA
module Page
module Admin
class Settings < Page::Base
+ ##
+ # TODO, define all selectors required by this page object
+ #
+ # See gitlab-org/gitlab-qa#154
+ #
+ view 'app/views/admin/application_settings/show.html.haml'
+
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..ea4c920c82c 100644
--- a/qa/qa/page/base.rb
+++ b/qa/qa/page/base.rb
@@ -5,6 +5,9 @@ module QA
class Base
include Capybara::DSL
include Scenario::Actable
+ extend SingleForwardable
+
+ def_delegators :evaluator, :view, :views
def refresh
visit current_url
@@ -37,9 +40,39 @@ module QA
page.within(selector) { yield } if block_given?
end
+ def click_element(name)
+ find(Page::Element.new(name).selector_css).click
+ 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/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..71255b18362 100644
--- a/qa/qa/page/dashboard/projects.rb
+++ b/qa/qa/page/dashboard/projects.rb
@@ -2,6 +2,8 @@ module QA
module Page
module Dashboard
class Projects < Page::Base
+ view 'app/views/dashboard/projects/index.html.haml'
+
def go_to_project(name)
find_link(text: name).click
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..37ed3b35bce 100644
--- a/qa/qa/page/group/show.rb
+++ b/qa/qa/page/group/show.rb
@@ -2,6 +2,13 @@ module QA
module Page
module Group
class Show < Page::Base
+ ##
+ # TODO, define all selectors required by this page object
+ #
+ # See gitlab-org/gitlab-qa#154
+ #
+ view 'app/views/groups/show.html.haml'
+
def go_to_subgroup(name)
click_link name
end
diff --git a/qa/qa/page/main/login.rb b/qa/qa/page/main/login.rb
index f88325f408b..7b4c1603017 100644
--- a/qa/qa/page/main/login.rb
+++ b/qa/qa/page/main/login.rb
@@ -2,6 +2,18 @@ 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 :passowrd_field, 'password_field :password'
+ element :sign_in_button, 'submit "Sign in"'
+ end
+
def initialize
wait('.application', time: 500)
end
diff --git a/qa/qa/page/main/menu.rb b/qa/qa/page/main/menu.rb
deleted file mode 100644
index bc9c4ec1215..00000000000
--- a/qa/qa/page/main/menu.rb
+++ /dev/null
@@ -1,50 +0,0 @@
-module QA
- module Page
- module Main
- class Menu < Page::Base
- def go_to_groups
- within_top_menu { click_link 'Groups' }
- end
-
- def go_to_projects
- within_top_menu do
- click_link 'Projects'
- click_link 'Your projects'
- end
- end
-
- def go_to_admin_area
- within_top_menu { find('.admin-icon').click }
- end
-
- def sign_out
- within_user_menu do
- click_link('Sign out')
- end
- end
-
- def has_personal_area?
- page.has_selector?('.header-user-dropdown-toggle')
- end
-
- private
-
- def within_top_menu
- page.within('.navbar') do
- yield
- end
- end
-
- def within_user_menu
- within_top_menu do
- find('.header-user-dropdown-toggle').click
-
- page.within('.dropdown-menu-nav') do
- yield
- end
- end
- end
- end
- end
- end
-end
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
new file mode 100644
index 00000000000..40da4a53e8a
--- /dev/null
+++ b/qa/qa/page/menu/admin.rb
@@ -0,0 +1,22 @@
+module QA
+ module Page
+ module Menu
+ class Admin < Page::Base
+ ##
+ # TODO, define all selectors required by this page object
+ #
+ # See gitlab-org/gitlab-qa#154
+ #
+ view 'app/views/admin/dashboard/index.html.haml'
+
+ def go_to_license
+ click_link 'License'
+ end
+
+ def go_to_settings
+ click_link 'Settings'
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/menu/main.rb b/qa/qa/page/menu/main.rb
new file mode 100644
index 00000000000..f8978b8a5f7
--- /dev/null
+++ b/qa/qa/page/menu/main.rb
@@ -0,0 +1,71 @@
+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"'
+ 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_element :groups_link }
+ end
+
+ def go_to_projects
+ within_top_menu do
+ 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 { click_element :admin_area_link }
+ end
+
+ def sign_out
+ within_user_menu do
+ click_link('Sign out')
+ end
+ end
+
+ def has_personal_area?
+ page.has_selector?('.qa-user-avatar')
+ end
+
+ private
+
+ def within_top_menu
+ page.within('.qa-navbar') do
+ yield
+ end
+ end
+
+ def within_user_menu
+ within_top_menu do
+ click_element :user_avatar
+
+ page.within('.dropdown-menu-nav') do
+ yield
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/menu/side.rb b/qa/qa/page/menu/side.rb
new file mode 100644
index 00000000000..1df4e0c2429
--- /dev/null
+++ b/qa/qa/page/menu/side.rb
@@ -0,0 +1,35 @@
+module QA
+ module Page
+ module Menu
+ class Side < Page::Base
+ view 'app/views/layouts/nav/sidebar/_project.html.haml' do
+ element :settings_item
+ element :repository_link, "title: 'Repository'"
+ element :top_level_items, '.sidebar-top-level-items'
+ end
+
+ def click_repository_setting
+ hover_setting do
+ click_link('Repository')
+ end
+ end
+
+ private
+
+ def hover_setting
+ within_sidebar do
+ find('.qa-settings-item').hover
+
+ yield
+ end
+ end
+
+ def within_sidebar
+ page.within('.sidebar-top-level-items') do
+ yield
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/project/new.rb b/qa/qa/page/project/new.rb
index b31bec27b59..9b1438f76d5 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'
+ 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
+
+ first('li', text: Runtime::Namespace.path).click
end
def choose_name(name)
diff --git a/qa/qa/page/project/settings/common.rb b/qa/qa/page/project/settings/common.rb
new file mode 100644
index 00000000000..5d1d5120929
--- /dev/null
+++ b/qa/qa/page/project/settings/common.rb
@@ -0,0 +1,17 @@
+module QA
+ module Page
+ module Project
+ module Settings
+ module Common
+ def expand(selector)
+ page.within('#content-body') do
+ find(selector).click
+
+ yield
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/project/settings/deploy_keys.rb b/qa/qa/page/project/settings/deploy_keys.rb
new file mode 100644
index 00000000000..a8d6f09777c
--- /dev/null
+++ b/qa/qa/page/project/settings/deploy_keys.rb
@@ -0,0 +1,34 @@
+module QA
+ module Page
+ module Project
+ module Settings
+ class DeployKeys < Page::Base
+ ##
+ # TODO, define all selectors required by this page object
+ #
+ # See gitlab-org/gitlab-qa#154
+ #
+ view 'app/views/projects/deploy_keys/edit.html.haml'
+
+ def fill_key_title(title)
+ fill_in 'deploy_key_title', with: title
+ end
+
+ def fill_key_value(key)
+ fill_in 'deploy_key_key', with: key
+ end
+
+ def add_key
+ click_on 'Add key'
+ end
+
+ def has_key_title?(title)
+ page.within('.deploy-keys') do
+ page.find('.title', text: title)
+ 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
new file mode 100644
index 00000000000..524d87c6be9
--- /dev/null
+++ b/qa/qa/page/project/settings/repository.rb
@@ -0,0 +1,24 @@
+module QA
+ module Page
+ module Project
+ module Settings
+ class Repository < Page::Base
+ include Common
+
+ ##
+ # TODO, define all selectors required by this page object
+ #
+ # See gitlab-org/gitlab-qa#154
+ #
+ view 'app/views/projects/settings/repository/show.html.haml'
+
+ def expand_deploy_keys(&block)
+ expand('.qa-expand-deploy-keys') do
+ DeployKeys.perform(&block)
+ 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..c8af5ba6280 100644
--- a/qa/qa/page/project/show.rb
+++ b/qa/qa/page/project/show.rb
@@ -2,8 +2,21 @@ module QA
module Page
module Project
class Show < Page::Base
+ view 'app/views/shared/_clone_panel.html.haml' do
+ element :clone_dropdown
+ element :clone_options_dropdown, '.clone-options-dropdown'
+ end
+
+ view 'app/views/shared/_clone_panel.html.haml' do
+ element :project_repository_location, 'text_field_tag :project_clone'
+ end
+
+ view 'app/views/projects/_home_panel.html.haml' do
+ element :project_name
+ end
+
def choose_repository_clone_http
- find('#clone-dropdown').click
+ click_element :clone_dropdown
page.within('.clone-options-dropdown') do
click_link('HTTP')
@@ -15,7 +28,7 @@ module QA
end
def project_name
- find('.project-title').text
+ find('.qa-project-name').text
end
def wait_for_push
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/browser.rb b/qa/qa/runtime/browser.rb
index 220bb45741b..14b2a488760 100644
--- a/qa/qa/runtime/browser.rb
+++ b/qa/qa/runtime/browser.rb
@@ -38,22 +38,49 @@ module QA
Capybara.register_driver :chrome do |app|
capabilities = Selenium::WebDriver::Remote::Capabilities.chrome(
- 'chromeOptions' => {
- 'args' => %w[headless no-sandbox disable-gpu window-size=1280,1680]
+ # This enables access to logs with `page.driver.manage.get_log(:browser)`
+ loggingPrefs: {
+ browser: "ALL",
+ client: "ALL",
+ driver: "ALL",
+ server: "ALL"
}
)
- Capybara::Selenium::Driver
- .new(app, browser: :chrome, desired_capabilities: capabilities)
- end
+ options = Selenium::WebDriver::Chrome::Options.new
+ options.add_argument("window-size=1240,1680")
- Capybara::Screenshot.register_driver(:chrome) do |driver, path|
- driver.browser.save_screenshot(path)
+ # Chrome won't work properly in a Docker container in sandbox mode
+ options.add_argument("no-sandbox")
+
+ # Run headless by default unless CHROME_HEADLESS is false
+ if QA::Runtime::Env.chrome_headless?
+ options.add_argument("headless")
+
+ # Chrome documentation says this flag is needed for now
+ # https://developers.google.com/web/updates/2017/04/headless-chrome#cli
+ options.add_argument("disable-gpu")
+ end
+
+ # Disable /dev/shm use in CI. See https://gitlab.com/gitlab-org/gitlab-ee/issues/4252
+ options.add_argument("disable-dev-shm-usage") if QA::Runtime::Env.running_in_ci?
+
+ Capybara::Selenium::Driver.new(
+ app,
+ browser: :chrome,
+ desired_capabilities: capabilities,
+ options: options
+ )
end
# Keep only the screenshots generated from the last failing test suite
Capybara::Screenshot.prune_strategy = :keep_last_run
+ # From https://github.com/mattheworiordan/capybara-screenshot/issues/84#issuecomment-41219326
+ Capybara::Screenshot.register_driver(:chrome) do |driver, path|
+ driver.browser.save_screenshot(path)
+ end
+
Capybara.configure do |config|
config.default_driver = :chrome
config.javascript_driver = :chrome
diff --git a/qa/qa/runtime/env.rb b/qa/qa/runtime/env.rb
new file mode 100644
index 00000000000..d5c28e9a7db
--- /dev/null
+++ b/qa/qa/runtime/env.rb
@@ -0,0 +1,15 @@
+module QA
+ module Runtime
+ module Env
+ extend self
+
+ def chrome_headless?
+ (ENV['CHROME_HEADLESS'] =~ /^(false|no|0)$/i) != 0
+ end
+
+ def running_in_ci?
+ ENV['CI'] || ENV['CI_SERVER']
+ 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/user.rb b/qa/qa/runtime/user.rb
index 60027c89ab1..2832439d9e0 100644
--- a/qa/qa/runtime/user.rb
+++ b/qa/qa/runtime/user.rb
@@ -10,6 +10,17 @@ 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/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/specs/features/login/standard_spec.rb b/qa/qa/specs/features/login/standard_spec.rb
index 9eaa2b772e6..141ffa3cfb7 100644
--- a/qa/qa/specs/features/login/standard_spec.rb
+++ b/qa/qa/specs/features/login/standard_spec.rb
@@ -7,7 +7,7 @@ module QA
# TODO, since `Signed in successfully` message was removed
# this is the only way to tell if user is signed in correctly.
#
- Page::Main::Menu.perform do |menu|
+ Page::Menu::Main.perform do |menu|
expect(menu).to have_personal_area
end
end
diff --git a/qa/qa/specs/features/mattermost/group_create_spec.rb b/qa/qa/specs/features/mattermost/group_create_spec.rb
index b3dbe44bf6e..2e27a285223 100644
--- a/qa/qa/specs/features/mattermost/group_create_spec.rb
+++ b/qa/qa/specs/features/mattermost/group_create_spec.rb
@@ -3,7 +3,7 @@ module QA
scenario 'creating a group with a mattermost team' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- Page::Main::Menu.act { go_to_groups }
+ Page::Menu::Main.act { go_to_groups }
Page::Dashboard::Groups.perform do |page|
page.go_to_new_group
diff --git a/qa/qa/specs/features/project/add_deploy_key_spec.rb b/qa/qa/specs/features/project/add_deploy_key_spec.rb
new file mode 100644
index 00000000000..43a85213501
--- /dev/null
+++ b/qa/qa/specs/features/project/add_deploy_key_spec.rb
@@ -0,0 +1,22 @@
+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
+
+ Page::Project::Settings::Repository.perform do |setting|
+ setting.expand_deploy_keys do |page|
+ expect(page).to have_key_title(deploy_key_title)
+ end
+ end
+ 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/spec/factory/base_spec.rb b/qa/spec/factory/base_spec.rb
index a3ba0176819..90dd58e20fd 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)
@@ -59,30 +60,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/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/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/env_spec.rb b/qa/spec/runtime/env_spec.rb
new file mode 100644
index 00000000000..57a72a04507
--- /dev/null
+++ b/qa/spec/runtime/env_spec.rb
@@ -0,0 +1,64 @@
+describe QA::Runtime::Env do
+ before do
+ allow(ENV).to receive(:[]).and_call_original
+ end
+
+ describe '.chrome_headless?' do
+ context 'when there is an env variable set' do
+ it 'returns false when falsey values specified' do
+ stub_env('CHROME_HEADLESS', 'false')
+ expect(described_class.chrome_headless?).to be_falsey
+
+ stub_env('CHROME_HEADLESS', 'no')
+ expect(described_class.chrome_headless?).to be_falsey
+
+ stub_env('CHROME_HEADLESS', '0')
+ expect(described_class.chrome_headless?).to be_falsey
+ end
+
+ it 'returns true when anything else specified' do
+ stub_env('CHROME_HEADLESS', 'true')
+ expect(described_class.chrome_headless?).to be_truthy
+
+ stub_env('CHROME_HEADLESS', '1')
+ expect(described_class.chrome_headless?).to be_truthy
+
+ stub_env('CHROME_HEADLESS', 'anything')
+ expect(described_class.chrome_headless?).to be_truthy
+ end
+ end
+
+ context 'when there is no env variable set' do
+ it 'returns the default, true' do
+ stub_env('CHROME_HEADLESS', nil)
+ expect(described_class.chrome_headless?).to be_truthy
+ end
+ end
+ end
+
+ describe '.running_in_ci?' do
+ context 'when there is an env variable set' do
+ it 'returns true if CI' do
+ stub_env('CI', 'anything')
+ expect(described_class.running_in_ci?).to be_truthy
+ end
+
+ it 'returns true if CI_SERVER' do
+ stub_env('CI_SERVER', 'anything')
+ expect(described_class.running_in_ci?).to be_truthy
+ end
+ end
+
+ context 'when there is no env variable set' do
+ it 'returns true' do
+ stub_env('CI', nil)
+ stub_env('CI_SERVER', nil)
+ expect(described_class.running_in_ci?).to be_falsey
+ end
+ end
+ end
+
+ def stub_env(name, value)
+ allow(ENV).to receive(:[]).with(name).and_return(value)
+ 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