page_magic 2.0.0.alpha1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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