Commit 814edbd7 authored by Dan Davison's avatar Dan Davison

Merge branch 'dj-chemlab-capybara-integration' into 'master'

Integrate Chemlab into QA test suite

See merge request gitlab-org/gitlab!54589
parents df5ccc67 79dabcdc
......@@ -149,7 +149,7 @@
= link_to edit_group_path(@group) do
.nav-icon-container
= sprite_icon('settings')
%span.nav-item-name.qa-group-settings-item
%span.nav-item-name{ data: { qa_selector: 'group_settings' } }
= _('Settings')
%ul.sidebar-sub-level-items.qa-group-sidebar-submenu{ data: { testid: 'group-settings-menu' } }
= nav_link(path: %w[groups#projects groups#edit badges#index ci_cd#show groups/applications#index], html_options: { class: "fly-out-top-item" } ) do
......
---
stage: none
group: unassigned
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
---
# Migration Guide Capybara → Chemlab
Given the view:
*_form.html*
```html
<form id="my-form">
<label for="first-name">First name</label>
<input type="text" name="first-name" data-qa-selector="first_name" />
<label for="last-name">Last name</label>
<input type="text" name="last-name" data-qa-selector="last_name" />
<label for="company-name">Company name</label>
<input type="text" name="company-name" data-qa-selector="company_name" />
<label for="user-name">User name</label>
<input type="text" name="user-name" data-qa-selector="user_name" />
<label for="password">Password</label>
<input type="password" name="password" data-qa-selector="password" />
<input type="submit" value="Continue" data-qa-selector="continue"/>
</form>
```
| Capybara | Chemlab |
| ------ | ----- |
| ![before](img/gl-capybara_V13_12.png) | ![after](img/gl-chemlab_V13_12.png) |
<!--
```ruby
# frozen_string_literal: true
module QA
module Page
class Form < Page::Base
view '_form.html' do
element :first_name
element :last_name
element :company_name
element :user_name
element :password
element :continue
end
end
end
end
```
```ruby
# frozen_string_literal: true
module QA
module Page
class Form < Chemlab::Page
text_field :first_name
text_field :last_name
text_field :company_name
text_field :user_name
text_field :password
button :continue
end
end
end
```
-->
## Key Differences
### Page Library Design vs Page Object Design
Page Objects as implemented in the existing framework require you to define methods to perform actions on elements. (Usually one-liners)
```ruby
def set_first_name(first_name)
fill_element(:first_name, first_name)
end
def click_continue
click_element(:continue)
end
it 'sets first name and clicks continue' do
Page::Form.perform do |form|
form.set_first_name('First Name')
form.click_continue
end
end
```
Page Libraries make this more efficient by providing methods based on the page's elements, making extra methods unnecessary.
```ruby
it 'sets first name and clicks continue' do
Page::Form.perform do |form|
form.first_name = 'First Name' # sets the first_name
form.continue # clicks Continue
end
end
```
Consider if we needed to validate the text of the `First name` field using Capybara. We'd need to add a one-liner to fetch the text:
```ruby
def get_first_name
find_element(:first_name).text
end
Page::Form.perform do |form|
form.set_first_name('First Name')
expect(form.get_first_name).to eq('First Name')
form.click_continue
end
```
Instead, because the page library automatically creates methods from page elements, we can fetch the text by calling `first_name` without writing code to define the method ourselves:
```ruby
Page::Form.perform do |form|
form.first_name = 'First Name'
expect(form.first_name).to eq('First Name')
form.continue
end
```
### Element Naming Convention
Since the element type is preserved within the Page Library, there is no need to specify a `_field` or `_button` suffix to the data-qa-selector.
```html
<!-- Before -->
<input type="text" name="first-name" data-qa-selector="first_name_field" />
<input type="submit" name="continue" value="Continue" data-qa-selector="continue_button" />
<!-- After -->
<input type="text" name="first-name" data-qa-selector="first_name" />
<input type="submit" name="continue" value="Continue" data-qa-selector="continue" />
```
This makes it much easier for Developers to write tests and contributes to testability since we can write the Page Library while we look at the UI.
......@@ -32,6 +32,6 @@
- if show_billing_in_sidebar? && administration_nav_item_disabled
= nav_link(path: 'billings#index') do
= link_to group_billings_path(@group), title: _('Billing') do
= link_to group_billings_path(@group), title: _('Billing'), data: { qa_selector: 'billing_link' } do
%span
= _('Billing')
......@@ -30,4 +30,4 @@
- if namespace.eligible_for_trial?
- glm_content = namespace_for_user ? 'user-billing' : 'group-billing'
%p= link_to 'Start your free trial', new_trial_registration_path(glm_source: 'gitlab.com', glm_content: glm_content), class: 'btn btn-confirm gl-button'
%p= link_to 'Start your free trial', new_trial_registration_path(glm_source: 'gitlab.com', glm_content: glm_content), class: 'btn btn-confirm gl-button', data: { qa_selector: 'start_your_free_trial' }
......@@ -4,7 +4,7 @@
.gl-banner-illustration
= custom_icon('trial_activated_banner')
.gl-banner-content.gl-display-flex.gl-flex-direction-column.gl-justify-content-center
%h4.gl-banner-title
%h4.gl-banner-title{ data: { qa_selector: 'alert_message' } }
= s_("BillingPlans|Congratulations, your free trial is activated.")
%button.gl-banner-close.close{ "aria-label" => _('Dismiss'), :type => "button" }
= sprite_icon('close', size: 16, css_class: 'gl-icon')
......@@ -15,32 +15,32 @@
- else
.form-group
= label_tag :first_name, _('First name'), for: :first_name, class: 'col-form-label'
= text_field_tag :first_name, params[:first_name] || current_user.first_name, class: 'form-control gl-form-input', required: true
= text_field_tag :first_name, params[:first_name] || current_user.first_name, class: 'form-control gl-form-input', required: true, data: { qa_selector: 'first_name' }
- if experiment_enabled?(:remove_known_trial_form_fields) && current_user.last_name.present?
= hidden_field_tag :last_name, current_user.last_name
- else
.form-group
= label_tag :last_name, _('Last name'), for: :last_name, class: 'col-form-label'
= text_field_tag :last_name, params[:last_name] || current_user.last_name, class: 'form-control gl-form-input', required: true
= text_field_tag :last_name, params[:last_name] || current_user.last_name, class: 'form-control gl-form-input', required: true, data: { qa_selector: 'last_name' }
- if experiment_enabled?(:remove_known_trial_form_fields) && current_user.organization.present?
= hidden_field_tag :company_name, current_user.organization
- else
.form-group
= label_tag :company_name, _('Company name'), for: :company_name, class: 'col-form-label'
= text_field_tag :company_name, params[:company_name] || current_user.organization, class: 'form-control gl-form-input', required: true
= text_field_tag :company_name, params[:company_name] || current_user.organization, class: 'form-control gl-form-input', required: true, data: { qa_selector: 'company_name' }
.form-group.gl-select2-html5-required-fix
= label_tag :company_size, _('Number of employees'), for: :company_size, class: 'col-form-label'
= select_tag :company_size, company_size_options_for_select(params[:company_size]), include_blank: true, class: 'select2', required: true
= select_tag :company_size, company_size_options_for_select(params[:company_size]), include_blank: true, class: 'select2', required: true, data: { qa_selector: 'number_of_employees' }
.form-group
= label_tag :phone_number, _('Telephone number'), for: :phone_number, class: 'col-form-label'
= telephone_field_tag :phone_number, params[:phone_number], pattern: '^(\+)*[0-9-\s]+$', class: 'form-control gl-form-input', required: true
= telephone_field_tag :phone_number, params[:phone_number], pattern: '^(\+)*[0-9-\s]+$', class: 'form-control gl-form-input', required: true, data: { qa_selector: 'telephone_number' }
%p.gray-500= _('Allowed characters: +, 0-9, -, and spaces.')
.form-group
= label_tag :number_of_users, _('How many users will be evaluating the trial?'), for: :number_of_users, class: 'col-form-label'
= number_field_tag :number_of_users, params[:number_of_users], class: 'form-control gl-form-input', required: true, min: 1
= number_field_tag :number_of_users, params[:number_of_users], class: 'form-control gl-form-input', required: true, min: 1, data: { qa_selector: 'number_of_users' }
.form-group.gl-select2-html5-required-fix
= label_tag :country, _('Country'), class: 'col-form-label'
= select_tag :country, options_for_select([[_('Please select a country'), '']]), class: 'select2 gl-transparent-pixel', required: true, id: 'country_select', data: { countries_end_point: countries_path, selected_option: params[:country]}
= submit_tag _('Continue'), class: 'btn gl-button btn-confirm btn-block'
= select_tag :country, options_for_select([[_('Please select a country'), '']]), class: 'select2 gl-transparent-pixel', required: true, id: 'country_select', data: { countries_end_point: countries_path, selected_option: params[:country], qa_selector: 'country' }
= submit_tag _('Continue'), class: 'btn gl-button btn-confirm btn-block', data: { qa_selector: 'continue' }
= render 'skip_trial'
......@@ -15,10 +15,10 @@
- if show_trial_namespace_select?
.form-group.gl-select2-html5-required-fix
= label_tag :namespace_id, _('This subscription is for'), for: :namespace_id, class: 'col-form-label'
= select_tag :namespace_id, namespace_options_for_select(params[:namespace_id]), class: 'select2', required: true
= select_tag :namespace_id, namespace_options_for_select(params[:namespace_id]), class: 'select2', required: true, data: { qa_selector: 'subscription_for' }
#group_name.form-group{ class: ("hidden" if show_trial_namespace_select?) }
= label_tag :new_group_name, _('New Group Name'), for: :new_group_name, class: 'col-form-label'
= text_field_tag :new_group_name, nil, class: 'form-control', required: !show_trial_namespace_select?
= text_field_tag :new_group_name, nil, class: 'form-control', required: !show_trial_namespace_select?, data: { qa_selector: 'new_group_name' }
- if should_ask_company_question?
.form-group
= label_tag :trial_entity, _('Is this GitLab trial for your company?')
......@@ -27,8 +27,8 @@
= radio_button_tag :trial_entity, :company, params[:trial_entity]=='company', required: true, class: 'form-check-input'
= label_tag :trial_entity_company, _('Yes'), class: 'form-check-label'
.gl-form-checkbox.form-check.form-check-inline
= radio_button_tag :trial_entity, :individual, params[:trial_entity]=='individual', required: true, class: 'form-check-input'
= radio_button_tag :trial_entity, :individual, params[:trial_entity]=='individual', required: true, class: 'form-check-input', data: { qa_selector: 'trial_individual' }
= label_tag :trial_entity_individual, _('No'), class: 'form-check-label'
= submit_tag _('Start your free trial'), class: 'gl-button btn btn-confirm btn-block'
= submit_tag _('Start your free trial'), class: 'gl-button btn btn-confirm btn-block', data: { qa_selector: 'start_your_free_trial' }
= render 'skip_trial'
......@@ -23,6 +23,9 @@ gem 'parallel', '~> 1.19'
gem 'rspec-parameterized', '~> 0.4.2'
gem 'github_api', '~> 0.18.2'
gem 'chemlab', '~> 0.5'
gem 'chemlab-library-www-gitlab-com', '~> 0.1'
group :development do
gem 'pry-byebug', '~> 3.5.1', platform: :mri
gem "ruby-debug-ide", "~> 0.7.0"
......
......@@ -33,6 +33,12 @@ GEM
capybara-screenshot (1.0.23)
capybara (>= 1.0, < 4)
launchy
chemlab (0.5.0)
rake (~> 12.3.0)
selenium-webdriver (~> 3.12)
watir (~> 6.17)
chemlab-library-www-gitlab-com (0.1.1)
chemlab (~> 0.4)
childprocess (3.0.0)
coderay (1.1.2)
concord (0.1.5)
......@@ -163,6 +169,9 @@ GEM
equalizer (~> 0.0.9)
parser (>= 2.6.5)
procto (~> 0.0.2)
watir (6.18.0)
regexp_parser (>= 1.2, < 3)
selenium-webdriver (>= 3.8)
xpath (3.2.0)
nokogiri (~> 1.8)
zeitwerk (2.4.2)
......@@ -175,6 +184,8 @@ DEPENDENCIES
airborne (~> 0.3.4)
capybara (~> 3.29.0)
capybara-screenshot (~> 1.0.23)
chemlab (~> 0.5)
chemlab-library-www-gitlab-com (~> 0.1)
faker (~> 1.6, >= 1.6.6)
github_api (~> 0.18.2)
gitlab-qa
......
......@@ -8,6 +8,8 @@ require_relative '../lib/gitlab'
require_relative '../lib/gitlab/utils'
require_relative '../config/initializers/0_inject_enterprise_edition_module'
require 'chemlab'
module QA
##
# Helper classes to represent frequently used sequences of actions
......@@ -428,6 +430,7 @@ module QA
module Alert
autoload :AutoDevopsAlert, 'qa/page/alert/auto_devops_alert'
autoload :FreeTrial, 'qa/page/alert/free_trial'
end
module Layout
......@@ -539,6 +542,11 @@ module QA
end
end
module Trials
autoload :New, 'qa/page/trials/new'
autoload :Select, 'qa/page/trials/select'
end
module Modal
autoload :DeleteWiki, 'qa/page/modal/delete_wiki'
end
......
......@@ -18,7 +18,7 @@ module QA
element :group_issues_item
element :group_sidebar
element :group_sidebar_submenu
element :group_settings_item
element :group_settings
end
view 'app/views/layouts/nav/sidebar/_wiki_link.html.haml' do
......@@ -33,6 +33,7 @@ module QA
view 'ee/app/views/groups/ee/_settings_nav.html.haml' do
element :ldap_synchronization_link
element :billing_link
end
view 'ee/app/views/layouts/nav/ee/_epic_link.html.haml' do
element :group_epics_link
......@@ -95,7 +96,7 @@ module QA
end
def go_to_ldap_sync_settings
hover_element(:group_settings_item) do
hover_element(:group_settings) do
within_submenu(:group_sidebar_submenu) do
click_element(:ldap_synchronization_link)
end
......@@ -117,7 +118,7 @@ module QA
end
def click_group_general_settings_item
hover_element(:group_settings_item) do
hover_element(:group_settings) do
within_submenu(:group_sidebar_submenu) do
click_element(:general_settings_link)
end
......@@ -159,6 +160,14 @@ module QA
click_element(:wiki_link)
end
end
def go_to_billing
hover_element(:group_settings) do
within_submenu(:group_sidebar_submenu) do
click_element(:billing_link)
end
end
end
end
end
end
......
# frozen_string_literal: true
module QA
module Page
module Alert
class FreeTrial < Chemlab::Page
# TODO: Supplant with data-qa-selectors
h4 :trial_activated_message, class: 'gl-banner-title'
end
end
end
end
......@@ -11,7 +11,7 @@ module QA
element :group_issues_item
element :group_members_item
element :group_milestones_link
element :group_settings_item
element :group_settings
end
view 'app/views/groups/sidebar/_packages_settings.html.haml' do
......@@ -31,7 +31,7 @@ module QA
def click_settings
within_sidebar do
click_element(:group_settings_item)
click_element(:group_settings)
end
end
......@@ -44,7 +44,7 @@ module QA
end
def click_group_general_settings_item
hover_element(:group_settings_item) do
hover_element(:group_settings) do
within_submenu(:group_sidebar_submenu) do
click_element(:general_settings_link)
end
......@@ -60,8 +60,8 @@ module QA
end
def go_to_package_settings
scroll_to_element(:group_settings_item)
hover_element(:group_settings_item) do
scroll_to_element(:group_settings)
hover_element(:group_settings) do
within_submenu(:group_sidebar_submenu) do
click_element(:group_package_settings_link)
end
......
# frozen_string_literal: true
module QA
module Page
module Group
module Settings
class Billing < Chemlab::Page
link :start_your_free_trial
end
end
end
end
end
# frozen_string_literal: true
module QA
module Page
module Trials
class New < Chemlab::Page
path '/-/trials/new'
# TODO: Supplant with data-qa-selectors
text_field :first_name, id: 'first_name'
text_field :last_name, id: 'last_name'
text_field :company_name, id: 'company_name'
select :number_of_employees, id: 'company_size'
text_field :telephone_number, id: 'phone_number'
text_field :number_of_users, id: 'number_of_users'
select :country, id: 'country_select'
button :continue, value: 'Continue'
end
end
end
end
# frozen_string_literal: true
module QA
module Page
module Trials
class Select < Chemlab::Page
path '/-/trials/select'
# TODO: Supplant with data-qa-selectors
select :subscription_for, id: 'namespace_id'
text_field :new_group_name, id: 'new_group_name'
button :start_your_free_trial, value: 'Start your free trial'
radio :trial_company, id: 'trial_entity_company'
radio :trial_individual, id: 'trial_entity_individual'
end
end
end
end
......@@ -6,6 +6,8 @@ require 'capybara/rspec'
require 'capybara-screenshot/rspec'
require 'selenium-webdriver'
require 'gitlab_handbook'
module QA
module Runtime
class Browser
......@@ -151,6 +153,12 @@ module QA
# fail because of unexpected line breaks and other white space
config.default_normalize_ws = true
end
Chemlab.configure do |config|
config.browser = Capybara.current_session.driver.browser # reuse Capybara session
config.libraries = [GitlabHandbook]
config.base_url = Runtime::Scenario.attributes[:gitlab_address] # reuse GitLab address
end
end
# rubocop: enable Metrics/AbcSize
......@@ -173,7 +181,7 @@ module QA
simulate_slow_connection if Runtime::Env.simulate_slow_connection?
page_class.validate_elements_present!
page_class.validate_elements_present! if page_class.respond_to?(:validate_elements_present!)
if QA::Runtime::Env.qa_cookies
browser = Capybara.current_session.driver.browser
......
......@@ -23,6 +23,15 @@ module QA
def perform(options, *args)
extract_address(:gitlab_address, options, args)
gitlab_address = URI(Runtime::Scenario.gitlab_address)
# Define the "About" page as an `about` subdomain.
# @example
# Given *gitlab_address* = 'https://gitlab.com/' #=> https://about.gitlab.com/
# Given *gitlab_address* = 'https://staging.gitlab.com/' #=> https://about.staging.gitlab.com/
# Given *gitlab_address* = 'http://gitlab-abc123.test/' #=> http://about.gitlab-abc123.test/
Runtime::Scenario.define(:about_address, URI(-> { gitlab_address.host = "about.#{gitlab_address.host}"; gitlab_address }.call).to_s) # rubocop:disable Style/Semicolon
##
# Perform before hooks, which are different for CE and EE
#
......
# frozen_string_literal: true
require 'securerandom'
module QA
RSpec.describe 'Fulfillment', :requires_admin, only: { subdomain: :staging } do
describe 'Purchase' do
let(:user) do
Resource::User.fabricate_via_api! do |user|
user.email = "gitlab-qa+#{SecureRandom.hex(2)}@gitlab.com"
user.api_client = Runtime::API::Client.as_admin
user.hard_delete_on_api_removal = true
end
end
let(:last_name) { 'Test' }
let(:company_name) { 'QA Test Company' }
let(:number_of_employees) { '500 - 1,999' }
let(:telephone_number) { '555-555-5555' }
let(:number_of_users) { 600 }
let(:country) { 'United States of America' }
let(:group) { Resource::Group.fabricate_via_api! }
before do
group.add_member(user)
Flow::Login.sign_in(as: user)
end
after do
user.remove_via_api!
group.remove_via_api!
end
describe 'starts a free trial' do
context 'when on about page' do
before do
Runtime::Browser.visit(:about, Chemlab::Vendor::GitlabHandbook::Page::About)
Chemlab::Vendor::GitlabHandbook::Page::About.perform(&:get_free_trial)
Page::Trials::New.perform(&:visit)
end
it 'registers for a new trial' do
Page::Trials::New.perform do |new|
# setter
new.company_name = company_name
new.number_of_employees = number_of_employees
new.telephone_number = telephone_number
new.number_of_users = number_of_users
new.country = country
new.continue
end
Page::Trials::Select.perform do |select|
select.new_group_name = group.name
select.trial_individual
select.start_your_free_trial
end
Page::Alert::FreeTrial.perform do |free_trial_alert|
expect(free_trial_alert.trial_activated_message).to have_text('Congratulations, your free trial is activated')
end
end
end
end
end
end
end
......@@ -3,6 +3,7 @@
RSpec.describe QA::Scenario::Template do
let(:feature) { spy('Runtime::Feature') }
let(:release) { spy('Runtime::Release') }
let(:gitlab_address) { 'https://gitlab.com/' }
before do
stub_const('QA::Runtime::Release', release)
......@@ -12,7 +13,7 @@ RSpec.describe QA::Scenario::Template do
end
it 'allows a feature to be enabled' do
subject.perform({ enable_feature: 'a-feature' })
subject.perform({ gitlab_address: gitlab_address, enable_feature: 'a-feature' })
expect(feature).to have_received(:enable).with('a-feature')
end
......@@ -21,7 +22,7 @@ RSpec.describe QA::Scenario::Template do
allow(QA::Runtime::Feature).to receive(:enabled?)
.with('another-feature').and_return(true)
subject.perform({ disable_feature: 'another-feature' })
subject.perform({ gitlab_address: gitlab_address, disable_feature: 'another-feature' })
expect(feature).to have_received(:disable).with('another-feature')
end
......@@ -30,7 +31,7 @@ RSpec.describe QA::Scenario::Template do
allow(QA::Runtime::Feature).to receive(:enabled?)
.with('another-feature').and_return(false)
subject.perform({ disable_feature: 'another-feature' })
subject.perform({ gitlab_address: gitlab_address, disable_feature: 'another-feature' })
expect(feature).not_to have_received(:disable).with('another-feature')
end
......@@ -38,7 +39,7 @@ RSpec.describe QA::Scenario::Template do
it 'ensures an enabled feature is disabled afterwards' do
allow(QA::Specs::Runner).to receive(:perform).and_raise('failed test')
expect { subject.perform({ enable_feature: 'a-feature' }) }.to raise_error('failed test')
expect { subject.perform({ gitlab_address: gitlab_address, enable_feature: 'a-feature' }) }.to raise_error('failed test')
expect(feature).to have_received(:enable).with('a-feature')
expect(feature).to have_received(:disable).with('a-feature')
......@@ -50,7 +51,7 @@ RSpec.describe QA::Scenario::Template do
allow(QA::Runtime::Feature).to receive(:enabled?)
.with('another-feature').and_return(true)
expect { subject.perform({ disable_feature: 'another-feature' }) }.to raise_error('failed test')
expect { subject.perform({ gitlab_address: gitlab_address, disable_feature: 'another-feature' }) }.to raise_error('failed test')
expect(feature).to have_received(:disable).with('another-feature')
expect(feature).to have_received(:enable).with('another-feature')
......@@ -62,9 +63,21 @@ RSpec.describe QA::Scenario::Template do
allow(QA::Runtime::Feature).to receive(:enabled?)
.with('another-feature').and_return(false)
expect { subject.perform({ disable_feature: 'another-feature' }) }.to raise_error('failed test')
expect { subject.perform({ gitlab_address: gitlab_address, disable_feature: 'another-feature' }) }.to raise_error('failed test')
expect(feature).not_to have_received(:disable).with('another-feature')
expect(feature).not_to have_received(:enable).with('another-feature')
end
it 'defines an about address by default' do
subject.perform( { gitlab_address: gitlab_address })
expect(QA::Runtime::Scenario.gitlab_address).to eq(gitlab_address)
expect(QA::Runtime::Scenario.about_address).to eq('https://about.gitlab.com/')
subject.perform({ gitlab_address: 'http://gitlab-abc.test/' })
expect(QA::Runtime::Scenario.gitlab_address).to eq('http://gitlab-abc.test/')
expect(QA::Runtime::Scenario.about_address).to eq('http://about.gitlab-abc.test/')
end
end
......@@ -18,6 +18,7 @@ module QA
stub_const('QA::Runtime::Scenario', attributes)
stub_const('QA::Runtime::Feature', feature)
allow(attributes).to receive(:gitlab_address).and_return(args[:gitlab_address])
allow(runner).to receive(:perform).and_yield(runner)
allow(QA::Runtime::Address).to receive(:valid?).and_return(true)
end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment