Commit e6c6acaf authored by Dan Davison's avatar Dan Davison

Merge branch 'qa-extend-click-visit-actions' into 'master'

Implement dynamic validation on QA Pages

Closes gitlab-qa#280

See merge request gitlab-org/gitlab-ce!25704
parents 2efc284a 83cfd20c
...@@ -130,6 +130,7 @@ module QA ...@@ -130,6 +130,7 @@ module QA
autoload :View, 'qa/page/view' autoload :View, 'qa/page/view'
autoload :Element, 'qa/page/element' autoload :Element, 'qa/page/element'
autoload :Validator, 'qa/page/validator' autoload :Validator, 'qa/page/validator'
autoload :Validatable, 'qa/page/validatable'
module Main module Main
autoload :Login, 'qa/page/main/login' autoload :Login, 'qa/page/main/login'
......
...@@ -13,7 +13,6 @@ module QA ...@@ -13,7 +13,6 @@ module QA
# The login page could take some time to load the first time it is visited. # The login page could take some time to load the first time it is visited.
# We visit the login page and wait for it to properly load only once before the tests. # We visit the login page and wait for it to properly load only once before the tests.
QA::Runtime::Browser.visit(:gitlab, QA::Page::Main::Login) QA::Runtime::Browser.visit(:gitlab, QA::Page::Main::Login)
QA::Page::Main::Login.perform(&:assert_page_loaded)
end end
end end
end end
......
...@@ -8,6 +8,7 @@ module QA ...@@ -8,6 +8,7 @@ module QA
prepend Support::Page::Logging if Runtime::Env.debug? prepend Support::Page::Logging if Runtime::Env.debug?
include Capybara::DSL include Capybara::DSL
include Scenario::Actable include Scenario::Actable
extend Validatable
extend SingleForwardable extend SingleForwardable
ElementNotFound = Class.new(RuntimeError) ElementNotFound = Class.new(RuntimeError)
...@@ -93,8 +94,10 @@ module QA ...@@ -93,8 +94,10 @@ module QA
find_element(name).set(false) find_element(name).set(false)
end end
def click_element(name) # replace with (..., page = self.class)
def click_element(name, page = nil)
find_element(name).click find_element(name).click
page.validate_elements_present! if page
end end
def fill_element(name, content) def fill_element(name, content)
......
# frozen_string_literal: true # frozen_string_literal: true
require 'active_support/core_ext/array/extract_options'
module QA module QA
module Page module Page
class Element class Element
attr_reader :name attr_reader :name, :attributes
def initialize(name, pattern = nil) def initialize(name, *options)
@name = name @name = name
@pattern = pattern || selector @attributes = options.extract_options!
@attributes[:pattern] ||= selector
options.each do |option|
if option.is_a?(String) || option.is_a?(Regexp)
@attributes[:pattern] = option
end
end
end end
def selector def selector
"qa-#{@name.to_s.tr('_', '-')}" "qa-#{@name.to_s.tr('_', '-')}"
end end
def required?
!!@attributes[:required]
end
def selector_css def selector_css
".#{selector}" ".#{selector}"
end end
def expression def expression
if @pattern.is_a?(String) if @attributes[:pattern].is_a?(String)
@_regexp ||= Regexp.new(Regexp.escape(@pattern)) @_regexp ||= Regexp.new(Regexp.escape(@attributes[:pattern]))
else else
@pattern @attributes[:pattern]
end end
end end
......
...@@ -39,19 +39,7 @@ module QA ...@@ -39,19 +39,7 @@ module QA
end end
view 'app/views/layouts/devise.html.haml' do view 'app/views/layouts/devise.html.haml' do
element :login_page element :login_page, required: true
end
def assert_page_loaded
unless page_loaded?
raise QA::Runtime::Browser::NotRespondingError, "Login page did not load at #{QA::Page::Main::Login.perform(&:current_url)}"
end
end
def page_loaded?
wait(max: 60) do
has_element?(:login_page)
end
end end
def sign_in_using_credentials(user = nil) def sign_in_using_credentials(user = nil)
...@@ -159,7 +147,7 @@ module QA ...@@ -159,7 +147,7 @@ module QA
fill_element :login_field, user.username fill_element :login_field, user.username
fill_element :password_field, user.password fill_element :password_field, user.password
click_element :sign_in_button click_element :sign_in_button, Page::Main::Menu
end end
def set_initial_password_if_present def set_initial_password_if_present
......
...@@ -10,15 +10,15 @@ module QA ...@@ -10,15 +10,15 @@ module QA
end end
view 'app/views/layouts/header/_default.html.haml' do view 'app/views/layouts/header/_default.html.haml' do
element :navbar element :navbar, required: true
element :user_avatar element :user_avatar, required: true
element :user_menu, '.dropdown-menu' # rubocop:disable QA/ElementWithPattern element :user_menu, '.dropdown-menu' # rubocop:disable QA/ElementWithPattern
end end
view 'app/views/layouts/nav/_dashboard.html.haml' do view 'app/views/layouts/nav/_dashboard.html.haml' do
element :admin_area_link element :admin_area_link
element :projects_dropdown element :projects_dropdown, required: true
element :groups_dropdown element :groups_dropdown, required: true
element :snippets_link element :snippets_link
end end
......
# frozen_string_literal: true
module QA
module Page
module Validatable
PageValidationError = Class.new(StandardError)
def validate_elements_present!
base_page = self.new
elements.each do |element|
next unless element.required?
# TODO: this wait needs to be replaced by the wait class
unless base_page.has_element?(element.name, wait: 60)
raise Validatable::PageValidationError, "#{element.name} did not appear on #{self.name} as expected"
end
end
end
end
end
end
...@@ -50,8 +50,8 @@ module QA ...@@ -50,8 +50,8 @@ module QA
@elements = [] @elements = []
end end
def element(name, pattern = nil) def element(name, *args)
@elements.push(Page::Element.new(name, pattern)) @elements.push(Page::Element.new(name, *args))
end end
end end
end end
......
...@@ -33,6 +33,7 @@ module QA ...@@ -33,6 +33,7 @@ module QA
def self.visit(address, page = nil, &block) def self.visit(address, page = nil, &block)
new.visit(address, page, &block) new.visit(address, page, &block)
page.validate_elements_present!
end end
def self.configure! def self.configure!
......
...@@ -4,11 +4,11 @@ module QA ...@@ -4,11 +4,11 @@ module QA
context 'Manage', :orchestrated, :mattermost do context 'Manage', :orchestrated, :mattermost do
describe 'Mattermost login' do describe 'Mattermost login' do
it 'user logs into Mattermost using GitLab OAuth' do it 'user logs into Mattermost using GitLab OAuth' do
Runtime::Browser.visit(:gitlab, Page::Main::Login) do Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials } Page::Main::Login.perform(&:sign_in_using_credentials)
Runtime::Browser.visit(:mattermost, Page::Mattermost::Login) do Runtime::Browser.visit(:mattermost, Page::Mattermost::Login)
Page::Mattermost::Login.act { sign_in_using_oauth } Page::Mattermost::Login.perform(&:sign_in_using_oauth)
Page::Mattermost::Main.perform do |page| Page::Mattermost::Main.perform do |page|
expect(page).to have_content(/(Welcome to: Mattermost|Logout GitLab Mattermost)/) expect(page).to have_content(/(Welcome to: Mattermost|Logout GitLab Mattermost)/)
...@@ -16,6 +16,4 @@ module QA ...@@ -16,6 +16,4 @@ module QA
end end
end end
end end
end
end
end end
...@@ -56,8 +56,11 @@ module QA ...@@ -56,8 +56,11 @@ module QA
elements elements
end end
def click_element(name) def click_element(name, page = nil)
log("clicking :#{name}") msg = ["clicking :#{name}"]
msg << ", expecting to be at #{page.class}" if page
log(msg.compact.join(' '))
super super
end end
......
...@@ -50,4 +50,60 @@ describe QA::Page::Element do ...@@ -50,4 +50,60 @@ describe QA::Page::Element do
expect(subject.matches?('some_name selector')).to be false expect(subject.matches?('some_name selector')).to be false
end end
end end
describe 'attributes' do
context 'element with no args' do
subject { described_class.new(:something) }
it 'defaults pattern to #selector' do
expect(subject.attributes[:pattern]).to eq 'qa-something'
expect(subject.attributes[:pattern]).to eq subject.selector
end
it 'is not required by default' do
expect(subject.required?).to be false
end
end
context 'element with a pattern' do
subject { described_class.new(:something, /link_to 'something'/) }
it 'has an attribute[pattern] of the pattern' do
expect(subject.attributes[:pattern]).to eq /link_to 'something'/
end
it 'is not required by default' do
expect(subject.required?).to be false
end
end
context 'element with requirement; no pattern' do
subject { described_class.new(:something, required: true) }
it 'has an attribute[pattern] of the selector' do
expect(subject.attributes[:pattern]).to eq 'qa-something'
expect(subject.attributes[:pattern]).to eq subject.selector
end
it 'is required' do
expect(subject.required?).to be true
end
end
context 'element with requirement and pattern' do
subject { described_class.new(:something, /link_to 'something_else_entirely'/, required: true) }
it 'has an attribute[pattern] of the passed pattern' do
expect(subject.attributes[:pattern]).to eq /link_to 'something_else_entirely'/
end
it 'is required' do
expect(subject.required?).to be true
end
it 'has a selector of the name' do
expect(subject.selector).to eq 'qa-something'
end
end
end
end end
# frozen_string_literal: true
require_relative '../../qa_helpers' require_relative '../../qa_helpers'
module RuboCop module RuboCop
module Cop module Cop
module QA module QA
# This cop checks for the usage of factories in migration specs # This cop checks for the usage of patterns in QA elements
# #
# @example # @example
# #
# # bad # # bad
# let(:user) { create(:user) } # element :some_element, "link_to 'something'"
# element :some_element, /link_to 'something'/
# #
# # good # # good
# let(:users) { table(:users) } # element :some_element
# let(:user) { users.create!(name: 'User 1', username: 'user1') } # element :some_element, required: true
class ElementWithPattern < RuboCop::Cop::Cop class ElementWithPattern < RuboCop::Cop::Cop
include QAHelpers include QAHelpers
...@@ -22,10 +25,13 @@ module RuboCop ...@@ -22,10 +25,13 @@ module RuboCop
return unless in_qa_file?(node) return unless in_qa_file?(node)
return unless method_name(node).to_s == 'element' return unless method_name(node).to_s == 'element'
element_name, pattern = node.arguments element_name, *args = node.arguments
return unless pattern
return if args.first.nil?
add_offense(node, location: pattern.source_range, message: MESSAGE % "qa-#{element_name.value.to_s.tr('_', '-')}") args.first.each_node(:str) do |arg|
add_offense(arg, message: MESSAGE % "qa-#{element_name.value.to_s.tr('_', '-')}")
end
end end
private private
......
# frozen_string_literal: true
require 'spec_helper' require 'spec_helper'
require 'rubocop' require 'rubocop'
...@@ -23,7 +25,7 @@ describe RuboCop::Cop::QA::ElementWithPattern do ...@@ -23,7 +25,7 @@ describe RuboCop::Cop::QA::ElementWithPattern do
element :groups_filter, 'search_field_tag :filter' element :groups_filter, 'search_field_tag :filter'
^^^^^^^^^^^^^^^^^^^^^^^^^^ Don't use a pattern for element, create a corresponding `qa-groups-filter` instead. ^^^^^^^^^^^^^^^^^^^^^^^^^^ Don't use a pattern for element, create a corresponding `qa-groups-filter` instead.
element :groups_filter_placeholder, /Search by name/ element :groups_filter_placeholder, /Search by name/
^^^^^^^^^^^^^^^^ Don't use a pattern for element, create a corresponding `qa-groups-filter-placeholder` instead. ^^^^^^^^^^^^^^ Don't use a pattern for element, create a corresponding `qa-groups-filter-placeholder` instead.
end end
RUBY RUBY
end end
...@@ -35,6 +37,13 @@ describe RuboCop::Cop::QA::ElementWithPattern do ...@@ -35,6 +37,13 @@ describe RuboCop::Cop::QA::ElementWithPattern do
element :groups_filter_placeholder element :groups_filter_placeholder
end end
RUBY RUBY
expect_no_offenses(<<-RUBY)
view 'app/views/shared/groups/_search_form.html.haml' do
element :groups_filter, required: true
element :groups_filter_placeholder, required: false
end
RUBY
end end
end 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