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,162 +1,122 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'active_support/inflector'
4
- require 'page_magic/element_definition_builder'
4
+ require_relative 'element_definition_builder'
5
+ require_relative 'elements/inheritance_hooks'
6
+ require_relative 'elements/config'
7
+ require_relative 'elements/types'
8
+
5
9
  module PageMagic
6
10
  # module Elements - contains methods that add element definitions to the objects it is mixed in to
7
11
  module Elements
8
- # hooks for objects that inherit classes that include the Elements module
9
- module InheritanceHooks
10
- # Copies parent element definitions on to subclass
11
- # @param [Class] clazz - inheritting class
12
- def inherited(clazz)
13
- clazz.element_definitions.merge!(element_definitions)
14
- end
15
- end
16
-
17
12
  INVALID_METHOD_NAME_MSG = 'a method already exists with this method name'
18
13
 
19
- # css
20
- # datalist_input
21
- # datalist_option
22
- # field
23
- # fieldset
24
- # file_field
25
- # fillable_field
26
- # frame
27
- # link_or_button
28
- # option
29
- # radio_button
30
- # select
31
- # table
32
- # table_row
33
- # xpath
34
- #
35
-
36
- TYPES = %i[field
37
- fieldset
38
- file_field
39
- fillable_field
40
- frame
41
- link_or_button
42
- option
43
- radio_button
44
- select
45
- table
46
- table_row
47
- text_field
48
- button
49
- link
50
- checkbox
51
- select_list
52
- radio
53
- textarea
54
- label].collect{ |type| [type, :"#{type}s"]}.flatten.freeze
55
-
56
14
  class << self
57
15
  def extended(clazz)
58
16
  clazz.extend(InheritanceHooks)
59
17
  end
18
+
19
+ private
20
+
21
+ def define_element_methods(types)
22
+ types.each { |type| alias_method type, :element }
23
+ end
24
+
25
+ def define_pluralised_element_methods(types)
26
+ types.each { |type| alias_method type.to_s.pluralize, :elements }
27
+ end
60
28
  end
61
29
 
62
- # Creates an element defintion.
63
- # Element defintions contain specifications for locating them and other sub elements.
64
- # if a block is specified then it will be executed against the element defintion.
30
+ # Creates an Element definition
65
31
  # This method is aliased to each of the names specified in {TYPES TYPES}
32
+ # Element definitions contain specifications for locating them and other sub elements.
33
+ # @yield if a block is specified then it will be executed against the element definition.
66
34
  # @example
67
35
  # element :widget, id: 'widget' do
68
36
  # link :next, text: 'next'
69
37
  # end
70
38
  # @overload element(name, selector, &block)
71
39
  # @param [Symbol] name the name of the element.
72
- # @param [Hash] selector a key value pair defining the method for locating this element
40
+ # @param [Hash<Symbol,String>] selector a key value pair defining the method for locating this element
73
41
  # @option selector [String] :text text contained within the element
74
42
  # @option selector [String] :css css selector
75
43
  # @option selector [String] :id the id of the element
76
44
  # @option selector [String] :name the value of the name attribute belonging to the element
77
45
  # @option selector [String] :label value of the label tied to the require field
46
+ # @param optional [Hash<Symbol,String>] capybara_options
78
47
  # @overload element(element_class, &block)
79
48
  # @param [ElementClass] element_class a custom class of element that inherits {Element}.
80
49
  # the name of the element is derived from the class name. the Class name coverted to snakecase.
81
50
  # The selector must be defined on the class itself.
51
+ # @param optional [Hash<Symbol,String>] capybara_options
82
52
  # @overload element(name, element_class, &block)
83
53
  # @param [Symbol] name the name of the element.
84
54
  # @param [ElementClass] element_class a custom class of element that inherits {Element}.
85
55
  # The selector must be defined on the class itself.
56
+ # @param optional [Hash<Symbol,String>] capybara_options
86
57
  # @overload element(name, element_class, selector, &block)
87
58
  # @param [Symbol] name the name of the element.
88
59
  # @param [ElementClass] element_class a custom class of element that inherits {Element}.
89
- # @param [Hash] selector a key value pair defining the method for locating this element. See above for details
90
- def element(*args, &block)
91
- block ||= proc {}
92
- options = compute_options(args.dup, __callee__)
93
-
94
- section_class = options.delete(:section_class)
95
-
96
- add_element_definition(options.delete(:name)) do |parent_element, *e_args|
97
- definition_class = Class.new(section_class) do
98
- parent_element(parent_element)
99
- class_exec(*e_args, &block)
100
- end
60
+ # @param optional [Hash<Symbol,String>] capybara_options
61
+ def element(*args, **capybara_options, &block)
62
+ define_element(*args,
63
+ type: __callee__,
64
+ query_class: PageMagic::Element::Query::SingleResult,
65
+ **capybara_options,
66
+ &block)
67
+ end
101
68
 
102
- ElementDefinitionBuilder.new(**options.merge(definition_class: definition_class))
103
- end
69
+ # see docs for {Elements#element}
70
+ def elements(*args, **capybara_options, &block)
71
+ define_element(*args,
72
+ type: __callee__.to_s.singularize.to_sym,
73
+ query_class: PageMagic::Element::Query::MultipleResults,
74
+ **capybara_options,
75
+ &block)
104
76
  end
105
77
 
106
- alias elements element
107
- TYPES.each { |type| alias_method type, :element }
78
+ define_element_methods(TYPES)
79
+ define_pluralised_element_methods(TYPES)
108
80
 
109
- # @return [Hash] element definition names mapped to blocks that can be used to create unique instances of
110
- # and {Element} definitions
81
+ # @return [Hash<Symbol,ElementDefinitionBuilder>] element definition names mapped to
82
+ # blocks that can be used to create unique instances of {Element} definitions
111
83
  def element_definitions
112
84
  @element_definitions ||= {}
113
85
  end
114
86
 
115
87
  private
116
88
 
117
- def compute_options(args, type)
118
- section_class = remove_argument(args, Class) || Element
119
-
120
- { name: compute_name(args, section_class),
121
- type: type,
122
- selector: compute_selector(args, section_class),
123
- options: compute_argument(args, Hash),
124
- element: args.delete_at(0),
125
- section_class: section_class }.tap do |hash|
126
- hash[:options][:multiple_results] = type.to_s.end_with?('s')
127
- end
128
- end
129
-
130
- def add_element_definition(name, &block)
131
- raise InvalidElementNameException, 'duplicate page element defined' if element_definitions[name]
132
-
133
- methods = respond_to?(:instance_methods) ? instance_methods : methods()
134
- raise InvalidElementNameException, INVALID_METHOD_NAME_MSG if methods.find { |method| method == name }
135
-
136
- element_definitions[name] = block
137
- end
89
+ def define_element(*args, type:, query_class:, **capybara_options, &block)
90
+ block ||= proc {}
91
+ args << capybara_options unless capybara_options.empty?
92
+ config = validate!(args, type)
138
93
 
139
- def compute_name(args, section_class)
140
- name = remove_argument(args, Symbol)
141
- name || section_class.name.demodulize.underscore.to_sym unless section_class.is_a?(Element)
142
- end
94
+ element_definitions[config.name] = proc do |parent_element, *e_args|
95
+ config.definition_class = Class.new(config.element_class) do
96
+ parent_element(parent_element)
97
+ class_exec(*e_args, &block)
98
+ end
143
99
 
144
- def compute_selector(args, section_class)
145
- selector = remove_argument(args, Hash)
146
- selector || section_class.selector if section_class.respond_to?(:selector)
100
+ ElementDefinitionBuilder.new(query_class: query_class, **config.element_options)
101
+ end
147
102
  end
148
103
 
149
104
  def method_added(method)
105
+ super
150
106
  raise InvalidMethodNameException, 'method name matches element name' if element_definitions[method]
151
107
  end
152
108
 
153
- def compute_argument(args, clazz)
154
- remove_argument(args, clazz) || clazz.new
109
+ def validate!(args, type)
110
+ config = Config.build(args, type).validate!
111
+ validate_element_name(config.name)
112
+ config
155
113
  end
156
114
 
157
- def remove_argument(args, clazz)
158
- argument = args.find { |arg| arg.is_a?(clazz) }
159
- args.delete(argument)
115
+ def validate_element_name(name)
116
+ raise InvalidElementNameException, 'duplicate page element defined' if element_definitions[name]
117
+
118
+ methods = respond_to?(:instance_methods) ? instance_methods : methods()
119
+ raise InvalidElementNameException, INVALID_METHOD_NAME_MSG if methods.find { |method| method == name }
160
120
  end
161
121
  end
162
122
  end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PageMagic
4
+ module Elements
5
+ CONFIG_STRUCT = Struct.new(:name,
6
+ :definition_class,
7
+ :type,
8
+ :selector,
9
+ :options,
10
+ :element,
11
+ :element_class,
12
+ keyword_init: true)
13
+
14
+ # class Config - use to validate input to {PageMagic::Elements#elment}
15
+ class Config < CONFIG_STRUCT
16
+ INVALID_SELECTOR_MSG = 'Pass a locator/define one on the class'
17
+ INVALID_ELEMENT_CLASS_MSG = 'Element class must be of type `PageMagic::Element`'
18
+ TYPE_REQUIRED_MESSAGE = 'element type required'
19
+
20
+ class << self
21
+ # Create `Config` used to build instances `PageMagic::Element` see `Page::Elements#element` for details
22
+ # @param [Args<Object>] args arguments passed to `Page::Elements#element`
23
+ # @return [Config]
24
+ def build(args, type)
25
+ element_class = remove_argument(args, Class) || Element
26
+ new(
27
+ name: compute_name(args, element_class),
28
+ type: type_for(type),
29
+ selector: compute_selector(args, element_class),
30
+ options: compute_argument(args, Hash),
31
+ element: args.delete_at(0),
32
+ element_class: element_class
33
+ )
34
+ end
35
+
36
+ private
37
+
38
+ def compute_name(args, element_class)
39
+ name = remove_argument(args, Symbol)
40
+ name || element_class.name.demodulize.underscore.to_sym unless element_class.is_a?(Element)
41
+ end
42
+
43
+ def compute_selector(args, element_class)
44
+ selector = remove_argument(args, Hash)
45
+ selector || element_class.selector if element_class.respond_to?(:selector)
46
+ end
47
+
48
+ def compute_argument(args, clazz)
49
+ remove_argument(args, clazz) || clazz.new
50
+ end
51
+
52
+ def remove_argument(args, clazz)
53
+ argument = args.find { |arg| arg.is_a?(clazz) }
54
+ args.delete(argument)
55
+ end
56
+
57
+ def type_for(type)
58
+ field?(type) ? :field : type
59
+ end
60
+
61
+ def field?(type)
62
+ %i[ text_field checkbox select_list radio textarea field file_field fillable_field
63
+ radio_button select].include?(type)
64
+ end
65
+ end
66
+
67
+ # Options for the building of `PageMagic::Element` via `PageMagic::ElementDefinitionBuilder#new`
68
+ # @return [Hash<Symbol,Object>]
69
+ def element_options
70
+ to_h.except(:element_class, :name, :type, :options).update(selector: selector)
71
+ end
72
+
73
+ # Selector built using supplied configuration
74
+ # @return [PageMagic::Element::Selector::Model]
75
+ def selector
76
+ selector = self[:selector]
77
+ Element::Selector.find(selector.keys.first).build(type, selector.values.first, options: options)
78
+ end
79
+
80
+ # Validate supplied configuration
81
+ # @raise [PageMagic::InvalidConfigurationException]
82
+ # @return [PageMagic::Elements::Config]
83
+ def validate!
84
+ raise PageMagic::InvalidConfigurationException, INVALID_SELECTOR_MSG unless element || valid_selector?
85
+ raise PageMagic::InvalidConfigurationException, 'element type required' unless type
86
+ raise PageMagic::InvalidConfigurationException, INVALID_ELEMENT_CLASS_MSG unless valid_element_class?
87
+
88
+ self
89
+ end
90
+
91
+ private
92
+
93
+ def valid_selector?
94
+ selector = self[:selector]
95
+ selector.is_a?(Hash) && !selector.empty?
96
+ end
97
+
98
+ def valid_element_class?
99
+ element_class && (element_class == PageMagic::Element || element_class < PageMagic::Element)
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PageMagic
4
+ module Elements
5
+ # hooks for objects that inherit classes that include the Elements module
6
+ module InheritanceHooks
7
+ # Copies parent element definitions on to subclass
8
+ # @param [Class] clazz - inheriting class
9
+ def inherited(clazz)
10
+ super
11
+ clazz.element_definitions.merge!(element_definitions)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PageMagic
4
+ module Elements
5
+ TYPES = %i[field
6
+ fieldset
7
+ file_field
8
+ fillable_field
9
+ frame
10
+ link_or_button
11
+ option
12
+ radio_button
13
+ select
14
+ table
15
+ table_row
16
+ text_field
17
+ button
18
+ link
19
+ checkbox
20
+ select_list
21
+ radio
22
+ textarea
23
+ label].freeze
24
+ end
25
+ end
@@ -36,4 +36,7 @@ module PageMagic
36
36
 
37
37
  class NotSupportedException < RuntimeError
38
38
  end
39
+
40
+ class InvalidConfigurationException < StandardError
41
+ end
39
42
  end
@@ -19,7 +19,7 @@ module PageMagic
19
19
  @browser_element = browser
20
20
  end
21
21
 
22
- # @return [Array] class level defined element definitions
22
+ # @return [Array<ElementDefinitionBuilder>] class level defined element definitions
23
23
  def element_definitions
24
24
  self.class.element_definitions
25
25
  end
@@ -37,7 +37,7 @@ module PageMagic
37
37
  element_context.send(method, *args)
38
38
  end
39
39
 
40
- def respond_to?(*args)
40
+ def respond_to_missing?(*args)
41
41
  contains_element?(args.first) || super
42
42
  end
43
43
 
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../active_support/core_ext/object/to_query'
4
+ require_relative 'comparator'
5
+
6
+
7
+ module PageMagic
8
+ # models mapping used to relate pages to uris
9
+ class Mapping
10
+ attr_reader :path, :parameters, :fragment
11
+
12
+ # @param [Object] path String or Regular expression to match with
13
+ # @param [Hash] parameters mapping of parameter name to literal or regex to match with
14
+ # @param [Object] fragment String or Regular expression to match with
15
+ # @raise [MatcherInvalidException] if at least one component is not specified
16
+ def initialize(path = nil, parameters: {}, fragment: nil)
17
+ raise MatcherInvalidException unless path || parameters || fragment
18
+
19
+ @path = Comparator.for(path)
20
+ @parameters = Comparator.for(parameters)
21
+ @fragment = Comparator.for(fragment)
22
+ end
23
+
24
+ # @return [Boolean] true if no component contains a Regexp
25
+ def can_compute_uri?
26
+ !fragment.fuzzy? && !path.fuzzy? && !parameters.fuzzy?
27
+ end
28
+
29
+ # @return [String] uri represented by this mapping
30
+ def compute_uri
31
+ path.to_s.dup.tap do |uri|
32
+ uri << "?#{parameters.comparator.to_query}" unless parameters.empty?
33
+ uri << "##{fragment}" if fragment.present?
34
+ end
35
+ end
36
+
37
+ # @return [Fixnum] hash for instance
38
+ def hash
39
+ [path, parameters, fragment].hash
40
+ end
41
+
42
+ # @param [String] uri
43
+ # @return [Boolean] returns true if the uri is matched against this matcher
44
+ def match?(uri)
45
+ uri = URI(uri)
46
+ path.match?(uri.path) && parameters.match?(parameters_hash(uri.query)) && fragment.match?(uri.fragment)
47
+ end
48
+
49
+ # compare this matcher against another
50
+ # @param [Mapping] other
51
+ # @return [Fixnum] -1 = smaller, 0 = equal to, 1 = greater than
52
+ def <=>(other)
53
+ path_comparison = path <=> other.path
54
+ return path_comparison unless path_comparison.zero?
55
+
56
+ parameter_comparison = parameters <=> other.parameters
57
+ return parameter_comparison unless parameter_comparison.zero?
58
+
59
+ fragment <=> other.fragment
60
+ end
61
+
62
+ # check equality
63
+ # @param [Mapping] other
64
+ # @return [Boolean]
65
+ def ==(other)
66
+ return false unless other.is_a?(Mapping)
67
+
68
+ path == other.path && parameters == other.parameters && fragment == other.fragment
69
+ end
70
+
71
+ alias eql? ==
72
+
73
+ private
74
+
75
+ def parameters_hash(string)
76
+ CGI.parse(string.to_s.downcase).collect { |key, value| [key.downcase, value.first] }.to_h
77
+ end
78
+ end
79
+ end