Permalink
Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
239 lines (224 sloc) 8.35 KB
require "watir"
module Tapestry
module_function
NATIVE_QUALIFIERS = %i[visible].freeze
def elements?
@elements
end
def recognizes?(method)
@elements.include? method.to_sym
end
def elements
@elements ||= Watir::Container.instance_methods unless @elements
end
module Element
# This iterator goes through the Watir container methods and
# provides a method for each so that Watir-based element names
# cane be defined on an interface definition, as part of an
# element definition.
Tapestry.elements.each do |element|
define_method(element) do |*signature, &block|
identifier, signature = parse_signature(signature)
context = context_from_signature(signature, &block)
define_element_accessor(identifier, signature, element, &context)
end
end
private
# A "signature" consists of a full element definition. For example:
#
# text_field :username, id: 'username'
#
# The signature of this element definition is:
#
# [:username, {:id=>"username"}]
#
# This is the identifier of the element (`username`) and the locator
# provided for it. This method separates out the identifier and the
# locator.
def parse_signature(signature)
[signature.shift, signature.shift]
end
# Returns the block or proc that serves as a context for an element
# definition. Consider the following element definitions:
#
# ul :facts, id: 'fact-list'
# span :fact, -> { facts.span(class: 'site-item')}
#
# Here the second element definition provides a proc that contains a
# context for another element definition. That leads to the following
# construction being sent to the browser:
#
# @browser.ul(id: 'fact-list').span(class: 'site-item')
def context_from_signature(*signature, &block)
if block_given?
block
else
context = signature.shift
context.is_a?(Proc) && signature.empty? ? context : nil
end
end
# This method provides the means to get the aspects of an accessor
# signature. The "aspects" refer to the locator information and any
# qualifier information that was provided along with the locator.
# This is important because the qualifier is not used to locate an
# element but rather to put conditions on how the state of the
# element is checked as it is being looked for.
#
# Note that "qualifiers" here refers to Watir boolean methods.
def accessor_aspects(element, *signature)
identifier = signature.shift
locator_args = {}
qualifier_args = {}
gather_aspects(identifier, element, locator_args, qualifier_args)
[locator_args, qualifier_args]
end
# This method is used to separate the two aspects of an accessor --
# the locators and the qualifiers. Part of this process involves
# querying the Watir driver library to determine what qualifiers
# it handles natively. Consider the following:
#
# select_list :accounts, id: 'accounts', selected: 'Select Option'
#
# Given that, this method will return with the following:
#
# locator_args: {:id=>"accounts"}
# qualifier_args: {:selected=>"Select Option"}
#
# Consider this:
#
# p :login_form, id: 'open', index: 0, visible: true
#
# Given that, this method will return with the following:
#
# locator_args: {:id=>"open", :index=>0, :visible=>true}
# qualifier_args: {}
#
# Notice that the `visible` qualifier is part of the locator arguments
# as opposed to being a qualifier argument, like `selected` was in the
# previous example. This is because Watir 6.x handles the `visible`
# qualifier natively. "Handling natively" means that when a qualifier
# is part of the locator, Watir knows how to intrpret the qualifier
# as a condition on the element, not as a way to locate the element.
def gather_aspects(identifier, element, locator_args, qualifier_args)
identifier.each_with_index do |hashes, index|
next if hashes.nil? || hashes.is_a?(Proc)
hashes.each do |k, v|
methods = Watir.element_class_for(element).instance_methods
if methods.include?(:"#{k}?") && !NATIVE_QUALIFIERS.include?(k)
qualifier_args[k] = identifier[index][k]
else
locator_args[k] = v
end
end
end
[locator_args, qualifier_args]
end
# Defines an accessor method for an element that allows the "friendly
# name" (identifier) of the element to be proxied to a Watir element
# object that corresponds to the element type. When this identifier
# is referenced, it generates an accessor method for that element
# in the browser. Consider this element definition defined on a class
# with an instance of `page`:
#
# text_field :username, id: 'username'
#
# This allows:
#
# page.username.set 'tester'
#
# So any element identifier can be called as if it were a method on
# the interface (class) on which it is defined. Because the method
# is proxied to Watir, you can use the full Watir API by calling
# methods (like `set`, `click`, etc) on the element identifier.
#
# It is also possible to have an element definition like this:
#
# text_field :password
#
# This would allow access like this:
#
# page.username(id: 'username').set 'tester'
#
# This approach would lead to the *values variable having an array
# like this: [{:id => 'username'}].
#
# A third approach would be to utilize one element definition within
# the context of another. Consider the following element definitions:
#
# article :practice, id: 'practice'
#
# a :page_link do |text|
# practice.a(text: text)
# end
#
# This would allow access like this:
#
# page.page_link('Drag and Drop').click
#
# This approach would lead to the *values variable having an array
# like this: ["Drag and Drop"].
def define_element_accessor(identifier, *signature, element, &block)
locators, qualifiers = accessor_aspects(element, signature)
define_method(identifier.to_s) do |*values|
if block_given?
instance_exec(*values, &block)
else
locators = values[0] if locators.empty?
access_element(element, locators, qualifiers)
end
end
end
end
module Locator
private
# This method is what actually calls the browser instance to find
# an element. If there is an element definition like this:
#
# text_field :username, id: 'username'
#
# This will become the following:
#
# browser.text_field(id: 'username')
#
# Note that the `to_subtype` method is called, which allows for the
# generic `element` to be expressed as the type of element, as opposed
# to `text_field` or `select_list`. For example, an `element` may be
# defined like this:
#
# element :enable, id: 'enableForm'
#
# Which means it would look like this:
#
# Watir::HTMLElement:0x1c8c9 selector={:id=>"enableForm"}
#
# Whereas getting the subtype would give you:
#
# Watir::CheckBox:0x12f8b elector={element: (webdriver element)}
#
# Which is what you would get if the element definition were this:
#
# checkbox :enable, id: 'enableForm'
#
# Using the subtype does get tricky for scripts that require the
# built-in sychronization aspects and wait states of Watir.
#
# The approach being used in this method is necessary to allow actions
# like `set`, which are not available on `element`, even though other
# actions, like `click`, are. But if you use `element` for your element
# definitions, and your script requires a series of actions where elements
# may be delayed in appearing, you'll get an "unable to locate element"
# message, along with a Watir::Exception::UnknownObjectException.
def access_element(element, locators, _qualifiers)
if element == "element".to_sym
@browser.element(locators).to_subtype
else
@browser.__send__(element, locators)
end
rescue Watir::Exception::UnknownObjectException
return false if caller_locations.any? do |str|
str.to_s.match?("ready_validations_pass?")
end
raise
end
end
end