2021-01-29 00:20:46 +05:30
---
stage: none
group: unassigned
2021-02-22 17:27:13 +05:30
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
2021-01-29 00:20:46 +05:30
---
2018-03-17 18:26:18 +05:30
# Page objects in GitLab QA
In GitLab QA we are using a known pattern, called _Page Objects_ .
2019-12-21 20:55:43 +05:30
This means that we have built an abstraction for all pages in GitLab that we use
2018-03-17 18:26:18 +05:30
to drive GitLab QA scenarios. Whenever we do something on a page, like filling
2020-10-24 23:57:45 +05:30
in a form or clicking a button, we do that only through a page object
2018-03-17 18:26:18 +05:30
associated with this area of GitLab.
For example, when GitLab QA test harness signs in into GitLab, it needs to fill
2020-10-24 23:57:45 +05:30
in user login and user password. To do that, we have a class, called
2018-03-17 18:26:18 +05:30
`Page::Main::Login` and `sign_in_using_credentials` methods, that is the only
2020-10-24 23:57:45 +05:30
piece of the code, that reads the `user_login` and `user_password`
2018-03-17 18:26:18 +05:30
fields.
## Why do we need that?
2020-10-24 23:57:45 +05:30
We need page objects because we need to reduce duplication and avoid problems
2021-02-22 17:27:13 +05:30
whenever someone changes some selectors in the GitLab source code.
2018-03-17 18:26:18 +05:30
Imagine that we have a hundred specs in GitLab QA, and we need to sign into
2020-10-24 23:57:45 +05:30
GitLab each time, before we make assertions. Without a page object, one would
2018-03-17 18:26:18 +05:30
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
2021-02-22 17:27:13 +05:30
this page to `t.text_field :username` it generates a different field
2018-03-17 18:26:18 +05:30
identifier, what would effectively break all tests.
2019-10-12 21:52:04 +05:30
Because we are using `Page::Main::Login.perform(&:sign_in_using_credentials)`
2020-10-24 23:57:45 +05:30
everywhere, when we want to sign in to GitLab, the page object is the single
2021-02-22 17:27:13 +05:30
source of truth, and we must update `fill_in :user_login`
to `fill_in :user_username` only in one place.
2018-03-17 18:26:18 +05:30
## 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
2021-02-22 17:27:13 +05:30
`t.text_field :username` in the _new session_ view we don't know about this
2018-03-17 18:26:18 +05:30
change until our GitLab QA nightly pipeline fails, or until someone triggers
2020-03-13 15:44:24 +05:30
`package-and-qa` action in their merge request.
2018-03-17 18:26:18 +05:30
2020-10-24 23:57:45 +05:30
Such a change would break all tests. We call this problem a _fragile
2018-03-17 18:26:18 +05:30
tests problem_.
2020-10-24 23:57:45 +05:30
To make GitLab QA more reliable and robust, we had to solve this
2018-03-17 18:26:18 +05:30
problem by introducing coupling between GitLab CE / EE views and GitLab QA.
## How did we solve fragile tests problem?
2021-02-22 17:27:13 +05:30
Currently, when you add a new `Page::Base` derived class, you must also
2020-10-24 23:57:45 +05:30
define all selectors that your page objects depend on.
2018-03-17 18:26:18 +05:30
Whenever you push your code to CE / EE repository, `qa:selectors` sanity test
2021-02-22 17:27:13 +05:30
job runs as a part of a CI pipeline.
2018-03-17 18:26:18 +05:30
2021-02-22 17:27:13 +05:30
This test validates all page objects that we have implemented in
`qa/page` directory. When it fails, it notifies you about missing
2020-10-24 23:57:45 +05:30
or invalid views/selectors definition.
2018-03-17 18:26:18 +05:30
## 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
2018-12-13 13:39:08 +05:30
element :password_field
element :password_confirmation
element :change_password_button
2018-03-17 18:26:18 +05:30
end
view 'app/views/devise/sessions/_new_base.html.haml' do
2018-12-13 13:39:08 +05:30
element :login_field
element :password_field
element :sign_in_button
2018-03-17 18:26:18 +05:30
end
# ...
2019-09-30 21:07:59 +05:30
end
2018-03-17 18:26:18 +05:30
end
end
```
2019-09-04 21:01:54 +05:30
### Defining Elements
2021-02-22 17:27:13 +05:30
The `view` DSL method corresponds to the Rails view, partial, or Vue component that renders the elements.
2018-12-13 13:39:08 +05:30
The `element` DSL method in turn declares an element for which a corresponding
2021-02-22 17:27:13 +05:30
`data-qa-selector=element_name_snaked` data attribute must be added to the view file.
2018-12-13 13:39:08 +05:30
You can also define a value (String or Regexp) to match to the actual view
code but **this is deprecated** in favor of the above method for two reasons:
- Consistency: there is only one way to define an element
2019-09-30 21:07:59 +05:30
- Separation of concerns: QA uses dedicated `data-qa-*` attributes instead of reusing code
2018-12-13 13:39:08 +05:30
or classes used by other components (e.g. `js-*` classes etc.)
2018-03-17 18:26:18 +05:30
```ruby
view 'app/views/my/view.html.haml' do
2019-09-30 21:07:59 +05:30
### Good ###
2019-10-12 21:52:04 +05:30
2019-09-30 21:07:59 +05:30
# Implicitly require the CSS selector `[data-qa-selector="logout_button"]` to be present in the view
2018-12-13 13:39:08 +05:30
element :logout_button
2019-09-30 21:07:59 +05:30
### Bad ###
2018-12-13 13:39:08 +05:30
## This is deprecated and forbidden by the `QA/ElementWithPattern` RuboCop cop.
2018-03-17 18:26:18 +05:30
# Require `f.submit "Sign in"` to be present in `my/view.html.haml
2018-12-13 13:39:08 +05:30
element :my_button, 'f.submit "Sign in"' # rubocop:disable QA/ElementWithPattern
2018-03-17 18:26:18 +05:30
2018-12-13 13:39:08 +05:30
## This is deprecated and forbidden by the `QA/ElementWithPattern` RuboCop cop.
2018-03-17 18:26:18 +05:30
# Match every line in `my/view.html.haml` against
# `/link_to .* "My Profile"/` regexp.
2018-12-13 13:39:08 +05:30
element :profile_link, /link_to .* "My Profile"/ # rubocop:disable QA/ElementWithPattern
2018-03-17 18:26:18 +05:30
end
```
2019-09-04 21:01:54 +05:30
### Adding Elements to a View
Given the following elements...
```ruby
view 'app/views/my/view.html.haml' do
element :login_field
element :password_field
element :sign_in_button
end
```
2020-10-24 23:57:45 +05:30
To add these elements to the view, you must change the Rails view, partial, or Vue component by adding a `data-qa-selector` attribute
2019-09-04 21:01:54 +05:30
for each element defined.
2019-09-30 21:07:59 +05:30
In our case, `data-qa-selector="login_field"` , `data-qa-selector="password_field"` and `data-qa-selector="sign_in_button"`
2019-09-04 21:01:54 +05:30
2019-09-30 21:07:59 +05:30
**app/views/my/view.html.haml**
2019-09-04 21:01:54 +05:30
```haml
2019-09-30 21:07:59 +05:30
= f.text_field :login, class: "form-control top", autofocus: "autofocus", autocapitalize: "off", autocorrect: "off", required: true, title: "This field is required.", data: { qa_selector: 'login_field' }
= f.password_field :password, class: "form-control bottom", required: true, title: "This field is required.", data: { qa_selector: 'password_field' }
= f.submit "Sign in", class: "btn btn-success", data: { qa_selector: 'sign_in_button' }
2019-09-04 21:01:54 +05:30
```
Things to note:
2020-06-23 00:09:42 +05:30
- The name of the element and the `qa_selector` must match and be snake_cased
2019-09-04 21:01:54 +05:30
- If the element appears on the page unconditionally, add `required: true` to the element. See
2019-09-30 21:07:59 +05:30
[Dynamic element validation ](dynamic_element_validation.md )
2020-03-13 15:44:24 +05:30
- You may see `.qa-selector` classes in existing Page Objects. We should prefer the [`data-qa-selector` ](#data-qa-selector-vs-qa-selector )
2019-09-30 21:07:59 +05:30
method of definition over the `.qa-selector` CSS class
### `data-qa-selector` vs `.qa-selector`
> Introduced in GitLab 12.1
There are two supported methods of defining elements within a view.
1. `data-qa-selector` attribute
1. `.qa-selector` class
Any existing `.qa-selector` class should be considered deprecated
and we should prefer the `data-qa-selector` method of definition.
2019-09-04 21:01:54 +05:30
2019-12-26 22:10:19 +05:30
### Dynamic element selection
> Introduced in GitLab 12.5
2020-03-13 15:44:24 +05:30
A common occurrence in automated testing is selecting a single "one-of-many" element.
In a list of several items, how do you differentiate what you are selecting on?
The most common workaround for this is via text matching. Instead, a better practice is
2019-12-26 22:10:19 +05:30
by matching on that specific element by a unique identifier, rather than by text.
We got around this by adding the `data-qa-*` extensible selection mechanism.
#### Examples
**Example 1**
Given the following Rails view (using GitLab Issues as an example):
```haml
%ul.issues-list
- @issues .each do |issue|
%li.issue{data: { qa_selector: 'issue', qa_issue_title: issue.title } }= link_to issue
```
We can select on that specific issue by matching on the Rails model.
```ruby
class Page::Project::Issues::Index < Page::Base
def has_issue?(issue)
has_element? :issue, issue_title: issue
end
end
```
In our test, we can validate that this particular issue exists.
```ruby
describe 'Issue' do
it 'has an issue titled "hello"' do
Page::Project::Issues::Index.perform do |index|
expect(index).to have_issue('hello')
end
end
end
```
**Example 2**
*By an index...*
```haml
%ol
- @some_model .each_with_index do |model, idx|
%li.model{ data: { qa_selector: 'model', qa_index: idx } }
```
```ruby
expect(the_page).to have_element(:model, index: 1) #=> select on the first model that appears in the list
```
2019-12-04 20:38:33 +05:30
### Exceptions
2020-10-24 23:57:45 +05:30
In some cases, it might not be possible or worthwhile to add a selector.
2019-12-04 20:38:33 +05:30
Some UI components use external libraries, including some maintained by third parties.
Even if a library is maintained by GitLab, the selector sanity test only runs
on code within the GitLab project, so it's not possible to specify the path for
the view for code in a library.
In such rare cases it's reasonable to use CSS selectors in page object methods,
with a comment explaining why an `element` can't be added.
2020-05-24 23:13:21 +05:30
### Define Page concerns
Some pages share common behaviors, and/or are prepended with EE-specific modules that adds EE-specific methods.
These modules must:
1. Extend from the `QA::Page::PageConcern` module, with `extend QA::Page::PageConcern` .
1. Override the `self.prepended` method if they need to `include` /`prepend` other modules themselves, and/or define
`view` or `elements` .
1. Call `super` as the first thing in `self.prepended` .
1. Include/prepend other modules and define their `view` /`elements` in a `base.class_eval` block to ensure they're
defined in the class that prepends the module.
2021-02-22 17:27:13 +05:30
These steps ensure the sanity selectors check detect problems properly.
2020-05-24 23:13:21 +05:30
For example, `qa/qa/ee/page/merge_request/show.rb` adds EE-specific methods to `qa/qa/page/merge_request/show.rb` (with
`QA::Page::MergeRequest::Show.prepend_if_ee('QA::EE::Page::MergeRequest::Show')` ) and following is how it's implemented
2020-06-23 00:09:42 +05:30
(only showing the relevant part and referring to the 4 steps described above with inline comments):
2020-05-24 23:13:21 +05:30
```ruby
module QA
module EE
module Page
module MergeRequest
module Show
extend QA::Page::PageConcern # 1.
def self.prepended(base) # 2.
super # 3.
base.class_eval do # 4.
prepend Page::Component::LicenseManagement
view 'app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue' do
element :head_mismatch, "The source branch HEAD has recently changed."
end
[...]
end
end
end
end
end
end
end
```
2018-03-17 18:26:18 +05:30
## 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?
2018-10-15 14:42:47 +05:30
If you need more information, ask for help on `#quality` channel on Slack
(internal, GitLab Team only).
2018-03-17 18:26:18 +05:30
If you are not a Team Member, and you still need help to contribute, please
2018-12-13 13:39:08 +05:30
open an issue in GitLab CE issue tracker with the `~QA` label.