page_magic 1.2.7 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (101) hide show
  1. checksums.yaml +5 -5
  2. data/.rubocop.yml +23 -4
  3. data/.simplecov +5 -3
  4. data/.zsh_config +6 -0
  5. data/Dockerfile +11 -0
  6. data/Gemfile +13 -13
  7. data/Gemfile.lock +135 -144
  8. data/Makefile +17 -0
  9. data/README.md +48 -4
  10. data/Rakefile +12 -2
  11. data/VERSION +1 -1
  12. data/circle.yml +4 -2
  13. data/lib/active_support/core_ext/object/to_query.rb +84 -0
  14. data/lib/page_magic.rb +22 -20
  15. data/lib/page_magic/class_methods.rb +5 -2
  16. data/lib/page_magic/comparator.rb +37 -0
  17. data/lib/page_magic/comparator/fuzzy.rb +23 -0
  18. data/lib/page_magic/comparator/literal.rb +22 -0
  19. data/lib/page_magic/comparator/null.rb +26 -0
  20. data/lib/page_magic/comparator/parameter_map.rb +52 -0
  21. data/lib/page_magic/driver.rb +3 -0
  22. data/lib/page_magic/drivers.rb +6 -5
  23. data/lib/page_magic/drivers/poltergeist.rb +2 -0
  24. data/lib/page_magic/drivers/rack_test.rb +3 -1
  25. data/lib/page_magic/drivers/selenium.rb +4 -2
  26. data/lib/page_magic/element.rb +35 -15
  27. data/lib/page_magic/element/locators.rb +8 -5
  28. data/lib/page_magic/element/not_found.rb +38 -0
  29. data/lib/page_magic/element/query.rb +21 -20
  30. data/lib/page_magic/element/query/multiple_results.rb +21 -0
  31. data/lib/page_magic/element/query/prefetched_result.rb +26 -0
  32. data/lib/page_magic/element/query/single_result.rb +20 -0
  33. data/lib/page_magic/element/selector.rb +41 -16
  34. data/lib/page_magic/element/selector/methods.rb +18 -0
  35. data/lib/page_magic/element/selector/model.rb +21 -0
  36. data/lib/page_magic/element_context.rb +7 -21
  37. data/lib/page_magic/element_definition_builder.rb +20 -24
  38. data/lib/page_magic/elements.rb +65 -68
  39. data/lib/page_magic/elements/config.rb +103 -0
  40. data/lib/page_magic/elements/inheritance_hooks.rb +15 -0
  41. data/lib/page_magic/elements/types.rb +25 -0
  42. data/lib/page_magic/exceptions.rb +6 -1
  43. data/lib/page_magic/instance_methods.rb +8 -3
  44. data/lib/page_magic/mapping.rb +79 -0
  45. data/lib/page_magic/session.rb +15 -35
  46. data/lib/page_magic/session_methods.rb +3 -1
  47. data/lib/page_magic/transitions.rb +49 -0
  48. data/lib/page_magic/utils/string.rb +18 -0
  49. data/lib/page_magic/utils/url.rb +20 -0
  50. data/lib/page_magic/wait_methods.rb +3 -0
  51. data/lib/page_magic/watcher.rb +12 -17
  52. data/lib/page_magic/watchers.rb +31 -15
  53. data/page_magic.gemspec +17 -13
  54. data/spec/lib/active_support/core_ext/object/to_query_test.rb +78 -0
  55. data/spec/page_magic/class_methods_spec.rb +66 -37
  56. data/spec/page_magic/comparator/fuzzy_spec.rb +44 -0
  57. data/spec/page_magic/comparator/literal_spec.rb +41 -0
  58. data/spec/page_magic/comparator/null_spec.rb +35 -0
  59. data/spec/page_magic/comparator/parameter_map_spec.rb +75 -0
  60. data/spec/page_magic/driver_spec.rb +26 -28
  61. data/spec/page_magic/drivers/poltergeist_spec.rb +6 -7
  62. data/spec/page_magic/drivers/rack_test_spec.rb +6 -9
  63. data/spec/page_magic/drivers/selenium_spec.rb +11 -12
  64. data/spec/page_magic/drivers_spec.rb +38 -29
  65. data/spec/page_magic/element/locators_spec.rb +28 -25
  66. data/spec/page_magic/element/not_found_spec.rb +24 -0
  67. data/spec/page_magic/element/query/multiple_results_spec.rb +14 -0
  68. data/spec/page_magic/element/query/single_result_spec.rb +21 -0
  69. data/spec/page_magic/element/query_spec.rb +26 -45
  70. data/spec/page_magic/element/selector_spec.rb +120 -110
  71. data/spec/page_magic/element_context_spec.rb +47 -87
  72. data/spec/page_magic/element_definition_builder_spec.rb +14 -71
  73. data/spec/page_magic/element_spec.rb +256 -0
  74. data/spec/page_magic/elements/config_spec.rb +200 -0
  75. data/spec/page_magic/elements_spec.rb +90 -127
  76. data/spec/page_magic/instance_methods_spec.rb +65 -63
  77. data/spec/page_magic/mapping_spec.rb +181 -0
  78. data/spec/page_magic/session_methods_spec.rb +29 -25
  79. data/spec/page_magic/session_spec.rb +109 -199
  80. data/spec/page_magic/transitions_spec.rb +43 -0
  81. data/spec/page_magic/utils/string_spec.rb +29 -0
  82. data/spec/page_magic/utils/url_spec.rb +9 -0
  83. data/spec/page_magic/wait_methods_spec.rb +16 -22
  84. data/spec/page_magic/watcher_spec.rb +22 -0
  85. data/spec/page_magic/watchers_spec.rb +58 -62
  86. data/spec/page_magic_spec.rb +31 -30
  87. data/spec/spec_helper.rb +9 -2
  88. data/spec/support/shared_contexts.rb +3 -1
  89. data/spec/support/shared_examples.rb +17 -17
  90. metadata +101 -48
  91. data/lib/page_magic/element/query_builder.rb +0 -48
  92. data/lib/page_magic/element/selector_methods.rb +0 -13
  93. data/lib/page_magic/matcher.rb +0 -121
  94. data/spec/element_spec.rb +0 -249
  95. data/spec/page_magic/element/query_builder_spec.rb +0 -108
  96. data/spec/page_magic/matcher_spec.rb +0 -336
  97. data/spec/support/shared_contexts/files_context.rb +0 -7
  98. data/spec/support/shared_contexts/nested_elements_html_context.rb +0 -16
  99. data/spec/support/shared_contexts/rack_application_context.rb +0 -9
  100. data/spec/support/shared_contexts/webapp_fixture_context.rb +0 -39
  101. data/spec/watcher_spec.rb +0 -61
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PageMagic
4
+ class Comparator
5
+ # class Map - used to model parameter matching requirements
6
+ class ParameterMap < Comparator
7
+ def initialize(map)
8
+ comparator = normalise(map).keys.each_with_object({}) do |key, params|
9
+ params[key] = Comparator.for(map[key])
10
+ end
11
+
12
+ fuzzy = comparator.values.any?(&:fuzzy?)
13
+ super(comparator, fuzzy)
14
+ end
15
+
16
+ def <=>(other)
17
+ return 0 if empty? && other.empty?
18
+ return 1 if other.empty?
19
+ if (comparator.keys.size <=> other.comparator.keys.size).zero?
20
+ return literal_matchers.size <=> other.literal_matchers.size
21
+ end
22
+
23
+ 0
24
+ end
25
+
26
+ def empty?
27
+ comparator.empty?
28
+ end
29
+
30
+ def literal_matchers
31
+ comparator.values.find_all { |matcher| !matcher.fuzzy? }
32
+ end
33
+
34
+ def match?(params)
35
+ params_copy = normalise(params)
36
+ comparator.each do |key, value|
37
+ param = params_copy[key]
38
+ return false unless value&.match?(param)
39
+ end
40
+ true
41
+ end
42
+
43
+ private
44
+
45
+ def normalise(hash)
46
+ hash.keys.each_with_object({}) do |key, map|
47
+ map[key.to_sym] = hash[key]
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -1,7 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module PageMagic
2
4
  # class Driver - instances are factories for drivers used by PageMagic
3
5
  class Driver
4
6
  attr_reader :supported_browsers, :handler
7
+
5
8
  # Creates a driver definition
6
9
  # @example
7
10
  # Driver.new do |rack_application, options|
@@ -1,4 +1,7 @@
1
- require 'page_magic/driver'
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'driver'
4
+ require_relative 'utils/string'
2
5
  module PageMagic
3
6
  # class Drivers - creates an object that can be used to hold driver definitions
4
7
  # These PageMagic gets the user's chosen driver from this object.
@@ -16,12 +19,10 @@ module PageMagic
16
19
  # Loads drivers defined in files at the given path
17
20
  # @param [String] path where the drivers are located
18
21
  def load(path = "#{__dir__}/drivers")
19
- require 'active_support/inflector'
20
-
21
- Dir["#{path}/*.rb"].each do |driver_file|
22
+ Dir["#{path}/*.rb"].sort.each do |driver_file|
22
23
  require driver_file
23
24
  driver_name = File.basename(driver_file)[/(.*)\.rb$/, 1]
24
- register self.class.const_get(driver_name.classify)
25
+ register self.class.const_get(Utils::String.classify(driver_name))
25
26
  end
26
27
  end
27
28
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  PageMagic::Drivers::Poltergeist = PageMagic::Driver.new(:poltergeist) do |app, options|
2
4
  require 'capybara/poltergeist'
3
5
  Capybara::Poltergeist::Driver.new(app, options)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  PageMagic::Drivers::RackTest = PageMagic::Driver.new(:rack_test) do |app, options|
2
- Capybara::RackTest::Driver.new(app, options)
4
+ Capybara::RackTest::Driver.new(app, **options)
3
5
  end
@@ -1,4 +1,6 @@
1
+ # frozen_string_literal: true
2
+
1
3
  PageMagic::Drivers::Selenium = PageMagic::Driver.new(:chrome, :firefox) do |app, options, browser|
2
- require 'watir-webdriver'
3
- Capybara::Selenium::Driver.new(app, options.dup.merge(browser: browser))
4
+ require 'watir'
5
+ Capybara::Selenium::Driver.new(app, **options.dup.merge(browser: browser))
4
6
  end
@@ -1,17 +1,26 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'forwardable'
2
- require 'page_magic/element/selector_methods'
3
- require 'page_magic/element/locators'
4
- require 'page_magic/element/selector'
5
- require 'page_magic/element/query_builder'
4
+ require_relative 'element/not_found'
5
+ require_relative 'element/selector/methods'
6
+ require_relative 'element/locators'
7
+ require_relative 'element/selector'
8
+ require_relative 'element/query'
6
9
  module PageMagic
7
10
  # class Element - represents an element in a html page.
8
11
  class Element
9
- EVENT_TYPES = [:set, :select, :select_option, :unselect_option, :click].freeze
12
+ EVENT_TYPES = %i[set select select_option unselect_option click].freeze
10
13
  DEFAULT_HOOK = proc {}.freeze
11
- EVENT_NOT_SUPPORTED_MSG = '%s event not supported'.freeze
14
+ EVENT_NOT_SUPPORTED_MSG = '%s event not supported'
12
15
 
13
- include SelectorMethods, Watchers, SessionMethods, WaitMethods, Locators
14
- extend Elements, SelectorMethods, Forwardable
16
+ include Locators
17
+ include WaitMethods
18
+ include SessionMethods
19
+ include Watchers
20
+ include Selector::Methods
21
+ extend Forwardable
22
+ extend Selector::Methods
23
+ extend Elements
15
24
 
16
25
  attr_reader :type, :name, :parent_element, :browser_element, :before_events, :after_events
17
26
 
@@ -24,7 +33,7 @@ module PageMagic
24
33
  # @!method before_events
25
34
  # If a block is passed in, it adds it to be run before an event is triggered on an element.
26
35
  # @see .after_events
27
- %i(after_events before_events).each do |method|
36
+ %i[after_events before_events].each do |method|
28
37
  define_method method do |&block|
29
38
  instance_variable_name = "@#{method}".to_sym
30
39
  instance_variable_value = instance_variable_get(instance_variable_name) || [DEFAULT_HOOK]
@@ -38,6 +47,7 @@ module PageMagic
38
47
  # @return [Element]
39
48
  def parent_element(page_element = nil)
40
49
  return @parent_page_element unless page_element
50
+
41
51
  @parent_page_element = page_element
42
52
  end
43
53
 
@@ -49,10 +59,16 @@ module PageMagic
49
59
  clazz.after_events.replace(after_events)
50
60
  end
51
61
 
62
+ def load(source)
63
+ new(Capybara::Node::Simple.new(source))
64
+ end
65
+
52
66
  # Defines watchers to be used by instances
53
67
  # @see Watchers#watch
54
68
  def watch(name, method = nil, &block)
55
- before_events { watch(name, method, &block) }
69
+ before_events do
70
+ watch(name, method: method, &block)
71
+ end
56
72
  end
57
73
 
58
74
  def ==(other)
@@ -78,9 +94,7 @@ module PageMagic
78
94
  # @raise [NotSupportedException] if the wrapped Capybara element does not support the method
79
95
  EVENT_TYPES.each do |method|
80
96
  define_method method do |*args|
81
- unless browser_element.respond_to?(method)
82
- raise NotSupportedException, EVENT_NOT_SUPPORTED_MSG % method
83
- end
97
+ raise NotSupportedException, EVENT_NOT_SUPPORTED_MSG % method unless browser_element.respond_to?(method)
84
98
 
85
99
  browser_element.send(method, *args)
86
100
  end
@@ -91,16 +105,21 @@ module PageMagic
91
105
  rescue ElementMissingException
92
106
  begin
93
107
  return browser_element.send(method, *args, &block) if browser_element.respond_to?(method)
94
- return parent_element.send(method, *args, &block)
108
+
109
+ parent_element.send(method, *args, &block)
95
110
  rescue NoMethodError
96
111
  super
97
112
  end
98
113
  end
99
114
 
100
- def respond_to?(*args)
115
+ def respond_to_missing?(*args)
101
116
  super || contains_element?(args.first) || browser_element.respond_to?(*args) || parent_element.respond_to?(*args)
102
117
  end
103
118
 
119
+ # def respond_to_missing?(*args)
120
+ # respond_to?(*args)
121
+ # end
122
+
104
123
  # @!method session
105
124
  # get the current session
106
125
  # @return [Session] returns the session of the parent page element.
@@ -131,6 +150,7 @@ module PageMagic
131
150
  def wrap_events(raw_element)
132
151
  EVENT_TYPES.each do |action_method|
133
152
  next unless raw_element.respond_to?(action_method)
153
+
134
154
  apply_hooks(raw_element: raw_element,
135
155
  capybara_method: action_method,
136
156
  before_events: before_events,
@@ -1,22 +1,25 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module PageMagic
2
4
  # for the benefit of pull review :@
3
5
  class Element
4
6
  # contains method for finding element definitions
5
7
  module Locators
6
8
  # message used when raising {ElementMissingException} from methods within this module
7
- ELEMENT_NOT_DEFINED_MSG = 'Element not defined: %s'.freeze
9
+ ELEMENT_NOT_DEFINED_MSG = 'Element not defined: %s'
8
10
 
9
11
  # find an element definition based on its name
10
12
  # @param [Symbol] name name of the element
11
13
  # @return [Element] element definition with the given name
12
14
  # @raise [ElementMissingException] raised when element with the given name is not found
13
15
  def element_by_name(name, *args)
14
- defintion = element_definitions[name]
15
- raise ElementMissingException, (ELEMENT_NOT_DEFINED_MSG % name) unless defintion
16
- defintion.call(self, *args)
16
+ definition = element_definitions[name]
17
+ raise ElementMissingException, (ELEMENT_NOT_DEFINED_MSG % name) unless definition
18
+
19
+ definition.call(self, *args)
17
20
  end
18
21
 
19
- # @return [Array] class level defined element definitions
22
+ # @return [Array<Element>] class level defined element definitions
20
23
  def element_definitions
21
24
  self.class.element_definitions
22
25
  end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PageMagic
4
+ class Element
5
+ # class NotFound - Used to represent elements which are missing. All method calls other than
6
+ # to those that check visibility thrown a {PageMagic::ElementMissingException} exception
7
+ class NotFound
8
+ # @private [Capybara::ElementNotFound] exception
9
+ def initialize(exception)
10
+ @exception = exception
11
+ end
12
+
13
+ # @return [Boolean] - always false
14
+ def visible?
15
+ false
16
+ end
17
+
18
+ # @return [Boolean] - always false
19
+ def present?
20
+ false
21
+ end
22
+
23
+ # @raise [PageMagic::ElementMissingException]
24
+ def method_missing(*_args)
25
+ raise ElementMissingException, exception.message
26
+ end
27
+
28
+ # @return [Boolean] - always true
29
+ def respond_to_missing?(*_args)
30
+ true
31
+ end
32
+
33
+ private
34
+
35
+ attr_reader :exception
36
+ end
37
+ end
38
+ end
@@ -1,35 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'query/multiple_results'
4
+ require_relative 'query/single_result'
5
+ require_relative 'query/prefetched_result'
1
6
  module PageMagic
2
7
  class Element
3
8
  # class Query - executes query on capybara driver
4
9
  class Query
5
- # Message template for execptions raised as a result of calling method_missing
6
- ELEMENT_NOT_FOUND_MSG = 'Unable to find %s'.freeze
7
-
8
- attr_reader :args, :multiple_results
10
+ attr_reader :selector_args, :options
9
11
 
10
- alias multiple_results? multiple_results
12
+ DEFAULT_DECORATOR = proc { |arg| arg }.freeze
11
13
 
12
- def initialize(args, multiple_results: false)
13
- @args = args
14
- @multiple_results = multiple_results
14
+ def initialize(*selector_args, options: {})
15
+ @selector_args = selector_args
16
+ @options = options
15
17
  end
16
18
 
17
- def execute(capybara_element)
18
- if multiple_results
19
- capybara_element.all(*args).to_a.tap do |result|
20
- raise Capybara::ElementNotFound if result.empty?
21
- end
22
- else
23
- capybara_element.find(*args)
24
- end
25
- rescue Capybara::Ambiguous => e
26
- raise AmbiguousQueryException, e.message
19
+ # TODO: - test for decoration?
20
+ # Run query against the scope of the given element
21
+ # The supplied block will be used to decorate the results
22
+ # @param [Capybara::Node::Element] capybara_element the element to be searched within
23
+ # @return [Array<Capybara::Node::Element>] the results
24
+ # @return [NullElement] when the element is not found
25
+ def execute(capybara_element, &block)
26
+ find(capybara_element, &(block || DEFAULT_DECORATOR))
27
27
  rescue Capybara::ElementNotFound => e
28
- raise ElementMissingException, e.message
28
+ NotFound.new(e)
29
29
  end
30
30
 
31
31
  def ==(other)
32
- other.respond_to?(:args) && args == other.args
32
+ other.respond_to?(:selector_args) && selector_args == other.selector_args &&
33
+ other.respond_to?(:options) && options == other.options
33
34
  end
34
35
  end
35
36
  end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PageMagic
4
+ class Element
5
+ class Query
6
+ # class MultipleResults - use to query for multiple results
7
+ class MultipleResults < Query
8
+ # Find multiple elements
9
+ # The supplied block will be used to decorate the results
10
+ # @param [Capybara::Node::Element] capybara_element the element to be searched within
11
+ # @return [Array<Capybara::Node::Element>] the results
12
+ def find(capybara_element, &block)
13
+ results = capybara_element.all(*selector_args, **options).to_a.tap do |result|
14
+ raise Capybara::ElementNotFound if result.empty?
15
+ end
16
+ results.collect { |result| block.call(result) }
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PageMagic
4
+ class Element
5
+ class Query
6
+ # class PrefetchedResult - used to return element that has already been retrieved
7
+ class PrefetchedResult < Query
8
+ def initialize(prefetched_element)
9
+ super
10
+ @prefetched_element = prefetched_element
11
+ end
12
+
13
+ # Returns the object provided to `initialize`
14
+ # The supplied block will be used to decorate the results
15
+ # @return [Capybara::Node::Element] the object supplied to `initialize`
16
+ def find(_capybara_element, &block)
17
+ block.call(prefetched_element)
18
+ end
19
+
20
+ private
21
+
22
+ attr_reader :prefetched_element
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PageMagic
4
+ class Element
5
+ class Query
6
+ # class SingleResult - use to query when only one result should be expected
7
+ class SingleResult < Query
8
+ # Find an element
9
+ # The supplied block will be used to decorate the results
10
+ # @param [Capybara::Node::Element] capybara_element the element to be searched within
11
+ # @return [Object] the results
12
+ def find(capybara_element, &block)
13
+ block.call capybara_element.find(*selector_args, **options)
14
+ rescue Capybara::Ambiguous => e
15
+ raise AmbiguousQueryException, e.message
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -1,40 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'selector/model'
1
4
  module PageMagic
5
+ # Capybara::Finder
2
6
  class Element
3
7
  # class Selector - models the selection criteria understood by Capybara
4
8
  class Selector
5
9
  class << self
6
- # Find a Selecor using it's name
10
+ # Find a Selector using it's name
7
11
  # @param [Symbol] name the name of the required Selector in snakecase format. See class constants for available
8
12
  # selectors
9
13
  # @return [Selector] returns the predefined selector with the given name
10
14
  def find(name)
11
- selector = constants.find { |constant| constant.to_s.casecmp(name.to_s).zero? }
12
- raise UnsupportedCriteriaException unless selector
13
- const_get(selector)
15
+ selector_name = selector_constant_name(name)
16
+ raise UnsupportedCriteriaException unless selector_name
17
+
18
+ const_get(selector_name)
14
19
  end
15
- end
16
20
 
17
- attr_reader :name, :formatter, :exact, :supports_type
21
+ private
22
+
23
+ def selector_constant_name(name)
24
+ constants.find { |constant| constant.to_s.casecmp(name.to_s).zero? }
25
+ end
26
+ end
18
27
 
28
+ # Initialize a new selector
29
+ # a block can be supplied to decorate the query. E.g.
30
+ # @example
31
+ # Selector.new(supports_type: false) do |arg|
32
+ # "*[name='#{arg}']"
33
+ # end
34
+ #
35
+ # @param [Symbol] selector the identifier for the selector
36
+ # @param [Boolean] supports_type whether the element type being searched for can be used as part of the query
37
+ # @param [Boolean] exact whether an exact match is required. E.g. element should include exactly the same text
19
38
  def initialize(selector = nil, supports_type: false, exact: false, &formatter)
20
- @name = selector
39
+ @selector = selector
21
40
  @formatter = formatter || proc { |arg| arg }
22
41
  @supports_type = supports_type
23
- @exact = exact
42
+ @options = {}.tap do |hash|
43
+ hash[:exact] = true if exact
44
+ end
24
45
  end
25
46
 
26
47
  # Build selector query parameters for Capybara's find method
27
48
  # @param [Symbol] element_type the type of browser element being found. e.g :link
28
- # @param [Hash] locator the selection method and its parameter. E.g. text: 'click me'
29
- def build(element_type, locator)
30
- [].tap do |array|
31
- array << element_type if supports_type
32
- array << name if name
33
- array << formatter.call(locator)
34
- array << { exact: true } if exact
35
- end
49
+ # @param [Hash<Symbol,String>] locator the selection method and its parameter. E.g. text: 'click me'
50
+ def build(element_type, locator, options: {})
51
+ array = [type(element_type), selector, formatter.call(locator)].compact
52
+ Model.new(array, self.options.merge(options))
53
+ end
54
+
55
+ private
56
+
57
+ def type(element_type)
58
+ supports_type ? element_type : nil
36
59
  end
37
60
 
61
+ attr_reader :supports_type, :options, :selector, :formatter
62
+
38
63
  XPATH = Selector.new(:xpath, supports_type: false)
39
64
  ID = Selector.new(:id, supports_type: false)
40
65
  LABEL = Selector.new(:field, supports_type: false, exact: true)