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,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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module PageMagic
2
4
  # class ElementContext - resolves which element definition to use when accessing the browser.
3
5
  class ElementContext
@@ -7,9 +9,9 @@ module PageMagic
7
9
  @page_element = page_element
8
10
  end
9
11
 
10
- # acts as proxy to element defintions defined on @page_element
11
- # @return [Object] result of callng method on page_element
12
- # @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
13
15
  # @return [Array<Element>] array of elements if more that one result was found the browser
14
16
  def method_missing(method, *args, &block)
15
17
  return page_element.send(method, *args, &block) if page_element.methods.include?(method)
@@ -18,27 +20,11 @@ module PageMagic
18
20
 
19
21
  super unless builder
20
22
 
21
- prefecteched_element = builder.element
22
- return builder.build(prefecteched_element) if prefecteched_element
23
-
24
- find(builder)
23
+ builder.build(page_element.browser_element)
25
24
  end
26
25
 
27
- def respond_to?(*args)
26
+ def respond_to_missing?(*args)
28
27
  page_element.respond_to?(*args) || super
29
28
  end
30
-
31
- private
32
-
33
- def find(builder)
34
- query = builder.build_query
35
- result = query.execute(page_element.browser_element)
36
-
37
- if query.multiple_results?
38
- result.collect { |e| builder.build(e) }
39
- else
40
- builder.build(result)
41
- end
42
- end
43
29
  end
44
30
  end
@@ -1,41 +1,37 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module PageMagic
2
4
  # Builder for creating ElementDefinitions
3
5
  class ElementDefinitionBuilder
4
- INVALID_SELECTOR_MSG = 'Pass a locator/define one on the class'.freeze
5
- attr_reader :definition_class, :options, :selector, :type, :element, :query_builder
6
-
7
- def initialize(definition_class:, selector:, type:, options: {}, element: nil)
8
- unless element
9
- selector ||= definition_class.selector
10
- raise UndefinedSelectorException, INVALID_SELECTOR_MSG if selector.nil? || selector.empty?
11
- end
12
-
6
+ def initialize(definition_class:, selector:, query_class: PageMagic::Element::Query::SingleResult, element: nil)
13
7
  @definition_class = definition_class
14
- @selector = selector
15
- @type = type
16
- @query_builder = Element::QueryBuilder.find(type)
17
-
18
- @options = { multiple_results: false }.merge(options)
19
- @element = element
20
- end
21
8
 
22
- # @return [Capybara::Query] query to find this element in the browser
23
- def build_query
24
- multiple_results = options.delete(:multiple_results)
25
- 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
26
14
  end
27
15
 
28
16
  # Create new instance of the ElementDefinition modeled by this builder
29
17
  # @param [Object] browser_element capybara browser element corresponding to the element modelled by this builder
30
- # @return [Element] element definition
18
+ # @return [Capybara::Node::Element]
19
+ # @return [Array<Capybara::Node::Element>]
31
20
  def build(browser_element)
32
- definition_class.new(browser_element)
21
+ query.execute(browser_element) do |result|
22
+ definition_class.new(result)
23
+ end
33
24
  end
34
25
 
35
26
  def ==(other)
36
27
  return false unless other.is_a?(ElementDefinitionBuilder)
37
- this = [options, selector, type, element, definition_class]
38
- this == [other.options, other.selector, other.type, other.element, other.definition_class]
28
+
29
+ this = [query, definition_class]
30
+ this == [other.send(:query), other.send(:definition_class)]
39
31
  end
32
+
33
+ private
34
+
35
+ attr_reader :query, :definition_class
40
36
  end
41
37
  end
@@ -1,125 +1,122 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_support/inflector'
2
- 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
+
3
9
  module PageMagic
4
10
  # module Elements - contains methods that add element definitions to the objects it is mixed in to
5
11
  module Elements
6
- # hooks for objects that inherit classes that include the Elements module
7
- module InheritanceHooks
8
- # Copies parent element definitions on to subclass
9
- # @param [Class] clazz - inheritting class
10
- def inherited(clazz)
11
- clazz.element_definitions.merge!(element_definitions)
12
- end
13
- end
14
-
15
- INVALID_METHOD_NAME_MSG = 'a method already exists with this method name'.freeze
16
-
17
- TYPES = [:text_field, :button, :link, :checkbox, :select_list, :radio, :textarea].collect do |type|
18
- [type, :"#{type}s"]
19
- end.flatten.freeze
12
+ INVALID_METHOD_NAME_MSG = 'a method already exists with this method name'
20
13
 
21
14
  class << self
22
15
  def extended(clazz)
23
16
  clazz.extend(InheritanceHooks)
24
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
25
28
  end
26
29
 
27
- # Creates an element defintion.
28
- # Element defintions contain specifications for locating them and other sub elements.
29
- # if a block is specified then it will be executed against the element defintion.
30
+ # Creates an Element definition
30
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.
31
34
  # @example
32
35
  # element :widget, id: 'widget' do
33
36
  # link :next, text: 'next'
34
37
  # end
35
38
  # @overload element(name, selector, &block)
36
39
  # @param [Symbol] name the name of the element.
37
- # @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
38
41
  # @option selector [String] :text text contained within the element
39
42
  # @option selector [String] :css css selector
40
43
  # @option selector [String] :id the id of the element
41
44
  # @option selector [String] :name the value of the name attribute belonging to the element
42
45
  # @option selector [String] :label value of the label tied to the require field
46
+ # @param optional [Hash<Symbol,String>] capybara_options
43
47
  # @overload element(element_class, &block)
44
48
  # @param [ElementClass] element_class a custom class of element that inherits {Element}.
45
49
  # the name of the element is derived from the class name. the Class name coverted to snakecase.
46
50
  # The selector must be defined on the class itself.
51
+ # @param optional [Hash<Symbol,String>] capybara_options
47
52
  # @overload element(name, element_class, &block)
48
53
  # @param [Symbol] name the name of the element.
49
54
  # @param [ElementClass] element_class a custom class of element that inherits {Element}.
50
55
  # The selector must be defined on the class itself.
56
+ # @param optional [Hash<Symbol,String>] capybara_options
51
57
  # @overload element(name, element_class, selector, &block)
52
58
  # @param [Symbol] name the name of the element.
53
59
  # @param [ElementClass] element_class a custom class of element that inherits {Element}.
54
- # @param [Hash] selector a key value pair defining the method for locating this element. See above for details
55
- def element(*args, &block)
56
- block ||= proc {}
57
- options = compute_options(args.dup, __callee__)
58
-
59
- section_class = options.delete(:section_class)
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
60
68
 
61
- add_element_definition(options.delete(:name)) do |parent_element, *e_args|
62
- definition_class = Class.new(section_class) do
63
- parent_element(parent_element)
64
- class_exec(*e_args, &block)
65
- end
66
- ElementDefinitionBuilder.new(options.merge(definition_class: definition_class))
67
- 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)
68
76
  end
69
77
 
70
- TYPES.each { |type| alias_method type, :element }
78
+ define_element_methods(TYPES)
79
+ define_pluralised_element_methods(TYPES)
71
80
 
72
- # @return [Hash] element definition names mapped to blocks that can be used to create unique instances of
73
- # 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
74
83
  def element_definitions
75
84
  @element_definitions ||= {}
76
85
  end
77
86
 
78
87
  private
79
88
 
80
- def compute_options(args, type)
81
- section_class = remove_argument(args, Class) || Element
82
-
83
- { name: compute_name(args, section_class),
84
- type: type,
85
- selector: compute_selector(args, section_class),
86
- options: compute_argument(args, Hash),
87
- element: args.delete_at(0),
88
- section_class: section_class }.tap do |hash|
89
- hash[:options][:multiple_results] = type.to_s.end_with?('s')
90
- end
91
- end
92
-
93
- def add_element_definition(name, &block)
94
- raise InvalidElementNameException, 'duplicate page element defined' if element_definitions[name]
95
-
96
- methods = respond_to?(:instance_methods) ? instance_methods : methods()
97
- raise InvalidElementNameException, INVALID_METHOD_NAME_MSG if methods.find { |method| method == name }
98
-
99
- element_definitions[name] = block
100
- 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)
101
93
 
102
- def compute_name(args, section_class)
103
- name = remove_argument(args, Symbol)
104
- name || section_class.name.demodulize.underscore.to_sym unless section_class.is_a?(Element)
105
- 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
106
99
 
107
- def compute_selector(args, section_class)
108
- selector = remove_argument(args, Hash)
109
- selector || section_class.selector if section_class.respond_to?(:selector)
100
+ ElementDefinitionBuilder.new(query_class: query_class, **config.element_options)
101
+ end
110
102
  end
111
103
 
112
104
  def method_added(method)
105
+ super
113
106
  raise InvalidMethodNameException, 'method name matches element name' if element_definitions[method]
114
107
  end
115
108
 
116
- def compute_argument(args, clazz)
117
- 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
118
113
  end
119
114
 
120
- def remove_argument(args, clazz)
121
- argument = args.find { |arg| arg.is_a?(clazz) }
122
- 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 }
123
120
  end
124
121
  end
125
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