page_magic 1.2.8 → 2.0.2

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 (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 +136 -148
  8. data/Makefile +17 -0
  9. data/README.md +26 -5
  10. data/Rakefile +12 -2
  11. data/VERSION +1 -1
  12. data/circle.yml +3 -1
  13. data/lib/active_support/core_ext/object/to_query.rb +84 -0
  14. data/lib/page_magic.rb +31 -24
  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 -69
  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 +15 -11
  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 +203 -0
  75. data/spec/page_magic/elements_spec.rb +90 -134
  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 +37 -29
  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,126 +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
- alias elements element
71
- TYPES.each { |type| alias_method type, :element }
78
+ define_element_methods(TYPES)
79
+ define_pluralised_element_methods(TYPES)
72
80
 
73
- # @return [Hash] element definition names mapped to blocks that can be used to create unique instances of
74
- # 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
75
83
  def element_definitions
76
84
  @element_definitions ||= {}
77
85
  end
78
86
 
79
87
  private
80
88
 
81
- def compute_options(args, type)
82
- section_class = remove_argument(args, Class) || Element
83
-
84
- { name: compute_name(args, section_class),
85
- type: type,
86
- selector: compute_selector(args, section_class),
87
- options: compute_argument(args, Hash),
88
- element: args.delete_at(0),
89
- section_class: section_class }.tap do |hash|
90
- hash[:options][:multiple_results] = type.to_s.end_with?('s')
91
- end
92
- end
93
-
94
- def add_element_definition(name, &block)
95
- raise InvalidElementNameException, 'duplicate page element defined' if element_definitions[name]
96
-
97
- methods = respond_to?(:instance_methods) ? instance_methods : methods()
98
- raise InvalidElementNameException, INVALID_METHOD_NAME_MSG if methods.find { |method| method == name }
99
-
100
- element_definitions[name] = block
101
- 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)
102
93
 
103
- def compute_name(args, section_class)
104
- name = remove_argument(args, Symbol)
105
- name || section_class.name.demodulize.underscore.to_sym unless section_class.is_a?(Element)
106
- 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
107
99
 
108
- def compute_selector(args, section_class)
109
- selector = remove_argument(args, Hash)
110
- selector || section_class.selector if section_class.respond_to?(:selector)
100
+ ElementDefinitionBuilder.new(query_class: query_class, **config.element_options)
101
+ end
111
102
  end
112
103
 
113
104
  def method_added(method)
105
+ super
114
106
  raise InvalidMethodNameException, 'method name matches element name' if element_definitions[method]
115
107
  end
116
108
 
117
- def compute_argument(args, clazz)
118
- 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
119
113
  end
120
114
 
121
- def remove_argument(args, clazz)
122
- argument = args.find { |arg| arg.is_a?(clazz) }
123
- 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 }
124
120
  end
125
121
  end
126
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] || definition_class.selector
77
+ raise PageMagic::InvalidConfigurationException, INVALID_SELECTOR_MSG unless validate_selector?(selector)
78
+
79
+ Element::Selector.find(selector.keys.first).build(type, selector.values.first, options: options)
80
+ end
81
+
82
+ # Validate supplied configuration
83
+ # @raise [PageMagic::InvalidConfigurationException]
84
+ # @return [PageMagic::Elements::Config]
85
+ def validate!
86
+ raise PageMagic::InvalidConfigurationException, 'element type required' unless type
87
+ raise PageMagic::InvalidConfigurationException, INVALID_ELEMENT_CLASS_MSG unless valid_element_class?
88
+
89
+ self
90
+ end
91
+
92
+ private
93
+
94
+ def validate_selector?(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