page_magic 2.0.0.alpha1 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +5 -2
  3. data/.zsh_config +5 -5
  4. data/Dockerfile +2 -1
  5. data/Gemfile +2 -1
  6. data/Gemfile.lock +9 -5
  7. data/Makefile +7 -3
  8. data/README.md +16 -4
  9. data/VERSION +1 -1
  10. data/lib/active_support/core_ext/object/to_query.rb +6 -6
  11. data/lib/page_magic.rb +15 -16
  12. data/lib/page_magic/class_methods.rb +1 -1
  13. data/lib/page_magic/comparator.rb +37 -0
  14. data/lib/page_magic/comparator/fuzzy.rb +23 -0
  15. data/lib/page_magic/comparator/literal.rb +22 -0
  16. data/lib/page_magic/comparator/null.rb +26 -0
  17. data/lib/page_magic/comparator/parameter_map.rb +52 -0
  18. data/lib/page_magic/drivers.rb +2 -2
  19. data/lib/page_magic/element.rb +19 -8
  20. data/lib/page_magic/element/locators.rb +4 -4
  21. data/lib/page_magic/element/not_found.rb +38 -0
  22. data/lib/page_magic/element/query.rb +19 -27
  23. data/lib/page_magic/element/query/multiple_results.rb +21 -0
  24. data/lib/page_magic/element/query/prefetched_result.rb +26 -0
  25. data/lib/page_magic/element/query/single_result.rb +20 -0
  26. data/lib/page_magic/element/selector.rb +38 -16
  27. data/lib/page_magic/element/selector/methods.rb +18 -0
  28. data/lib/page_magic/element/selector/model.rb +21 -0
  29. data/lib/page_magic/element_context.rb +5 -21
  30. data/lib/page_magic/element_definition_builder.rb +17 -24
  31. data/lib/page_magic/elements.rb +62 -102
  32. data/lib/page_magic/elements/config.rb +103 -0
  33. data/lib/page_magic/elements/inheritance_hooks.rb +15 -0
  34. data/lib/page_magic/elements/types.rb +25 -0
  35. data/lib/page_magic/exceptions.rb +3 -0
  36. data/lib/page_magic/instance_methods.rb +2 -2
  37. data/lib/page_magic/mapping.rb +79 -0
  38. data/lib/page_magic/session.rb +10 -32
  39. data/lib/page_magic/session_methods.rb +1 -1
  40. data/lib/page_magic/transitions.rb +49 -0
  41. data/lib/page_magic/utils/string.rb +4 -0
  42. data/lib/page_magic/utils/url.rb +20 -0
  43. data/lib/page_magic/watcher.rb +10 -17
  44. data/lib/page_magic/watchers.rb +28 -15
  45. data/spec/page_magic/class_methods_spec.rb +64 -37
  46. data/spec/page_magic/comparator/fuzzy_spec.rb +44 -0
  47. data/spec/page_magic/comparator/literal_spec.rb +41 -0
  48. data/spec/page_magic/comparator/null_spec.rb +35 -0
  49. data/spec/page_magic/comparator/parameter_map_spec.rb +75 -0
  50. data/spec/page_magic/driver_spec.rb +25 -29
  51. data/spec/page_magic/drivers/poltergeist_spec.rb +4 -7
  52. data/spec/page_magic/drivers/rack_test_spec.rb +4 -9
  53. data/spec/page_magic/drivers/selenium_spec.rb +9 -12
  54. data/spec/page_magic/drivers_spec.rb +36 -29
  55. data/spec/page_magic/element/locators_spec.rb +26 -25
  56. data/spec/page_magic/element/not_found_spec.rb +24 -0
  57. data/spec/page_magic/element/query/multiple_results_spec.rb +14 -0
  58. data/spec/page_magic/element/query/single_result_spec.rb +21 -0
  59. data/spec/page_magic/element/query_spec.rb +26 -47
  60. data/spec/page_magic/element/selector_spec.rb +118 -110
  61. data/spec/page_magic/element_context_spec.rb +46 -88
  62. data/spec/page_magic/element_definition_builder_spec.rb +12 -71
  63. data/spec/page_magic/element_spec.rb +256 -0
  64. data/spec/page_magic/elements/config_spec.rb +200 -0
  65. data/spec/page_magic/elements_spec.rb +87 -138
  66. data/spec/page_magic/instance_methods_spec.rb +63 -63
  67. data/spec/page_magic/mapping_spec.rb +181 -0
  68. data/spec/page_magic/session_methods_spec.rb +27 -25
  69. data/spec/page_magic/session_spec.rb +109 -198
  70. data/spec/page_magic/transitions_spec.rb +43 -0
  71. data/spec/page_magic/utils/string_spec.rb +20 -27
  72. data/spec/page_magic/utils/url_spec.rb +9 -0
  73. data/spec/page_magic/wait_methods_spec.rb +14 -22
  74. data/spec/page_magic/watcher_spec.rb +22 -0
  75. data/spec/page_magic/watchers_spec.rb +56 -62
  76. data/spec/page_magic_spec.rb +27 -24
  77. data/spec/spec_helper.rb +7 -3
  78. data/spec/support/shared_examples.rb +15 -17
  79. metadata +48 -15
  80. data/lib/page_magic/element/query_builder.rb +0 -61
  81. data/lib/page_magic/element/selector_methods.rb +0 -16
  82. data/lib/page_magic/matcher.rb +0 -130
  83. data/spec/element_spec.rb +0 -251
  84. data/spec/page_magic/element/query_builder_spec.rb +0 -110
  85. data/spec/page_magic/matcher_spec.rb +0 -338
  86. data/spec/support/shared_contexts/files_context.rb +0 -9
  87. data/spec/support/shared_contexts/nested_elements_html_context.rb +0 -18
  88. data/spec/support/shared_contexts/rack_application_context.rb +0 -11
  89. data/spec/support/shared_contexts/webapp_fixture_context.rb +0 -41
  90. data/spec/watcher_spec.rb +0 -64
@@ -1,10 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'forwardable'
4
- require 'page_magic/element/selector_methods'
5
- require 'page_magic/element/locators'
6
- require 'page_magic/element/selector'
7
- 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'
8
9
  module PageMagic
9
10
  # class Element - represents an element in a html page.
10
11
  class Element
@@ -16,9 +17,9 @@ module PageMagic
16
17
  include WaitMethods
17
18
  include SessionMethods
18
19
  include Watchers
19
- include SelectorMethods
20
+ include Selector::Methods
20
21
  extend Forwardable
21
- extend SelectorMethods
22
+ extend Selector::Methods
22
23
  extend Elements
23
24
 
24
25
  attr_reader :type, :name, :parent_element, :browser_element, :before_events, :after_events
@@ -58,10 +59,16 @@ module PageMagic
58
59
  clazz.after_events.replace(after_events)
59
60
  end
60
61
 
62
+ def load(source)
63
+ new(Capybara::Node::Simple.new(source))
64
+ end
65
+
61
66
  # Defines watchers to be used by instances
62
67
  # @see Watchers#watch
63
68
  def watch(name, method = nil, &block)
64
- before_events { watch(name, method, &block) }
69
+ before_events do
70
+ watch(name, method: method, &block)
71
+ end
65
72
  end
66
73
 
67
74
  def ==(other)
@@ -105,10 +112,14 @@ module PageMagic
105
112
  end
106
113
  end
107
114
 
108
- def respond_to?(*args)
115
+ def respond_to_missing?(*args)
109
116
  super || contains_element?(args.first) || browser_element.respond_to?(*args) || parent_element.respond_to?(*args)
110
117
  end
111
118
 
119
+ # def respond_to_missing?(*args)
120
+ # respond_to?(*args)
121
+ # end
122
+
112
123
  # @!method session
113
124
  # get the current session
114
125
  # @return [Session] returns the session of the parent page element.
@@ -13,13 +13,13 @@ module PageMagic
13
13
  # @return [Element] element definition with the given name
14
14
  # @raise [ElementMissingException] raised when element with the given name is not found
15
15
  def element_by_name(name, *args)
16
- defintion = element_definitions[name]
17
- raise ElementMissingException, (ELEMENT_NOT_DEFINED_MSG % name) unless defintion
16
+ definition = element_definitions[name]
17
+ raise ElementMissingException, (ELEMENT_NOT_DEFINED_MSG % name) unless definition
18
18
 
19
- defintion.call(self, *args)
19
+ definition.call(self, *args)
20
20
  end
21
21
 
22
- # @return [Array] class level defined element definitions
22
+ # @return [Array<Element>] class level defined element definitions
23
23
  def element_definitions
24
24
  self.class.element_definitions
25
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,44 +1,36 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'query/multiple_results'
4
+ require_relative 'query/single_result'
5
+ require_relative 'query/prefetched_result'
3
6
  module PageMagic
4
7
  class Element
5
8
  # class Query - executes query on capybara driver
6
9
  class Query
7
- # Message template for execptions raised as a result of calling method_missing
8
- ELEMENT_NOT_FOUND_MSG = 'Unable to find %s'
10
+ attr_reader :selector_args, :options
9
11
 
10
- attr_reader :args, :multiple_results
12
+ DEFAULT_DECORATOR = proc { |arg| arg }.freeze
11
13
 
12
- alias multiple_results? multiple_results
13
-
14
- def initialize(args, multiple_results: false)
15
- @args = args
16
- @multiple_results = multiple_results
14
+ def initialize(*selector_args, options: {})
15
+ @selector_args = selector_args
16
+ @options = options
17
17
  end
18
18
 
19
- def execute(capybara_element)
20
- if multiple_results
21
- capybara_element.all(*args).to_a.tap do |result|
22
- raise Capybara::ElementNotFound if result.empty?
23
- end
24
- else
25
- # TODO - make sure there is a test around this.
26
- if args.last.is_a?(Hash)
27
- capybara_element.find(*args[0...-1], **args.last)
28
- else
29
- capybara_element.find(*args)
30
- end
31
-
32
-
33
- end
34
- rescue Capybara::Ambiguous => e
35
- 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))
36
27
  rescue Capybara::ElementNotFound => e
37
- raise ElementMissingException, e.message
28
+ NotFound.new(e)
38
29
  end
39
30
 
40
31
  def ==(other)
41
- 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
42
34
  end
43
35
  end
44
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,43 +1,65 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'selector/model'
3
4
  module PageMagic
5
+ # Capybara::Finder
4
6
  class Element
5
7
  # class Selector - models the selection criteria understood by Capybara
6
8
  class Selector
7
9
  class << self
8
- # Find a Selecor using it's name
10
+ # Find a Selector using it's name
9
11
  # @param [Symbol] name the name of the required Selector in snakecase format. See class constants for available
10
12
  # selectors
11
13
  # @return [Selector] returns the predefined selector with the given name
12
14
  def find(name)
13
- selector = constants.find { |constant| constant.to_s.casecmp(name.to_s).zero? }
14
- raise UnsupportedCriteriaException unless selector
15
+ selector_name = selector_constant_name(name)
16
+ raise UnsupportedCriteriaException unless selector_name
15
17
 
16
- const_get(selector)
18
+ const_get(selector_name)
17
19
  end
18
- end
19
20
 
20
- attr_reader :name, :formatter, :exact, :supports_type
21
+ private
21
22
 
23
+ def selector_constant_name(name)
24
+ constants.find { |constant| constant.to_s.casecmp(name.to_s).zero? }
25
+ end
26
+ end
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
22
38
  def initialize(selector = nil, supports_type: false, exact: false, &formatter)
23
- @name = selector
39
+ @selector = selector
24
40
  @formatter = formatter || proc { |arg| arg }
25
41
  @supports_type = supports_type
26
- @exact = exact
42
+ @options = {}.tap do |hash|
43
+ hash[:exact] = true if exact
44
+ end
27
45
  end
28
46
 
29
47
  # Build selector query parameters for Capybara's find method
30
48
  # @param [Symbol] element_type the type of browser element being found. e.g :link
31
- # @param [Hash] locator the selection method and its parameter. E.g. text: 'click me'
32
- def build(element_type, locator)
33
- [].tap do |array|
34
- array << element_type if supports_type
35
- array << name if name
36
- array << formatter.call(locator)
37
- array << { exact: true } if exact
38
- 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))
39
53
  end
40
54
 
55
+ private
56
+
57
+ def type(element_type)
58
+ supports_type ? element_type : nil
59
+ end
60
+
61
+ attr_reader :supports_type, :options, :selector, :formatter
62
+
41
63
  XPATH = Selector.new(:xpath, supports_type: false)
42
64
  ID = Selector.new(:id, supports_type: false)
43
65
  LABEL = Selector.new(:field, supports_type: false, exact: true)
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PageMagic
4
+ class Element
5
+ class Selector
6
+ # module SelectorMethods - adds method for getting and setting an element selector
7
+ module Methods
8
+ # Gets/Sets a selector
9
+ # @param [Hash<Symbol,String>] selector method for locating the browser element. E.g. text: 'the text'
10
+ def selector(selector = nil)
11
+ return @selector unless selector
12
+
13
+ @selector = selector
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PageMagic
4
+ class Element
5
+ class Selector
6
+ # class model - represents the parameters for capybara finder methods
7
+ class Model
8
+ attr_reader :args, :options
9
+
10
+ def initialize(args, options = {})
11
+ @args = args
12
+ @options = options
13
+ end
14
+
15
+ def ==(other)
16
+ other.args == args && other.options == options
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -9,9 +9,9 @@ module PageMagic
9
9
  @page_element = page_element
10
10
  end
11
11
 
12
- # acts as proxy to element defintions defined on @page_element
13
- # @return [Object] result of callng method on page_element
14
- # @return [Element] animated page element containing located browser element
12
+ # acts as proxy to element definitions defined on @page_element
13
+ # @return [Object] result of calling method on page_element
14
+ # @return [Element] page element containing located browser element
15
15
  # @return [Array<Element>] array of elements if more that one result was found the browser
16
16
  def method_missing(method, *args, &block)
17
17
  return page_element.send(method, *args, &block) if page_element.methods.include?(method)
@@ -20,27 +20,11 @@ module PageMagic
20
20
 
21
21
  super unless builder
22
22
 
23
- prefecteched_element = builder.element
24
- return builder.build(prefecteched_element) if prefecteched_element
25
-
26
- find(builder)
23
+ builder.build(page_element.browser_element)
27
24
  end
28
25
 
29
- def respond_to?(*args)
26
+ def respond_to_missing?(*args)
30
27
  page_element.respond_to?(*args) || super
31
28
  end
32
-
33
- private
34
-
35
- def find(builder)
36
- query = builder.build_query
37
- result = query.execute(page_element.browser_element)
38
-
39
- if query.multiple_results?
40
- result.collect { |e| builder.build(e) }
41
- else
42
- builder.build(result)
43
- end
44
- end
45
29
  end
46
30
  end
@@ -3,42 +3,35 @@
3
3
  module PageMagic
4
4
  # Builder for creating ElementDefinitions
5
5
  class ElementDefinitionBuilder
6
- INVALID_SELECTOR_MSG = 'Pass a locator/define one on the class'
7
- attr_reader :definition_class, :options, :selector, :type, :element, :query_builder
8
-
9
- def initialize(definition_class:, selector:, type:, options: {}, element: nil)
10
- unless element
11
- selector ||= definition_class.selector
12
- raise UndefinedSelectorException, INVALID_SELECTOR_MSG if selector.nil? || selector.empty?
13
- end
14
-
6
+ def initialize(definition_class:, selector:, query_class: PageMagic::Element::Query::SingleResult, element: nil)
15
7
  @definition_class = definition_class
16
- @selector = selector
17
- @type = type
18
- @query_builder = Element::QueryBuilder.find(type)
19
-
20
- @options = { multiple_results: false }.merge(options)
21
- @element = element
22
- end
23
8
 
24
- # @return [Capybara::Query] query to find this element in the browser
25
- def build_query
26
- multiple_results = options.delete(:multiple_results)
27
- query_builder.build(selector, options, multiple_results: multiple_results)
9
+ @query = if element
10
+ PageMagic::Element::Query::PrefetchedResult.new(element)
11
+ else
12
+ query_class.new(*selector.args, options: selector.options)
13
+ end
28
14
  end
29
15
 
30
16
  # Create new instance of the ElementDefinition modeled by this builder
31
17
  # @param [Object] browser_element capybara browser element corresponding to the element modelled by this builder
32
- # @return [Element] element definition
18
+ # @return [Capybara::Node::Element]
19
+ # @return [Array<Capybara::Node::Element>]
33
20
  def build(browser_element)
34
- definition_class.new(browser_element)
21
+ query.execute(browser_element) do |result|
22
+ definition_class.new(result)
23
+ end
35
24
  end
36
25
 
37
26
  def ==(other)
38
27
  return false unless other.is_a?(ElementDefinitionBuilder)
39
28
 
40
- this = [options, selector, type, element, definition_class]
41
- this == [other.options, other.selector, other.type, other.element, other.definition_class]
29
+ this = [query, definition_class]
30
+ this == [other.send(:query), other.send(:definition_class)]
42
31
  end
32
+
33
+ private
34
+
35
+ attr_reader :query, :definition_class
43
36
  end
44
37
  end