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,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