page_magic 1.2.6 → 2.0.0.alpha1

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 (71) hide show
  1. checksums.yaml +5 -5
  2. data/.rubocop.yml +19 -3
  3. data/.simplecov +5 -3
  4. data/.zsh_config +6 -0
  5. data/Dockerfile +10 -0
  6. data/Gemfile +12 -12
  7. data/Gemfile.lock +133 -126
  8. data/Makefile +13 -0
  9. data/README.md +78 -14
  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 +7 -4
  15. data/lib/page_magic/class_methods.rb +4 -1
  16. data/lib/page_magic/driver.rb +3 -0
  17. data/lib/page_magic/drivers.rb +6 -5
  18. data/lib/page_magic/drivers/poltergeist.rb +2 -0
  19. data/lib/page_magic/drivers/rack_test.rb +3 -1
  20. data/lib/page_magic/drivers/selenium.rb +4 -2
  21. data/lib/page_magic/element.rb +19 -10
  22. data/lib/page_magic/element/locators.rb +4 -1
  23. data/lib/page_magic/element/query.rb +33 -33
  24. data/lib/page_magic/element/query_builder.rb +61 -0
  25. data/lib/page_magic/element/selector.rb +3 -0
  26. data/lib/page_magic/element/selector_methods.rb +3 -0
  27. data/lib/page_magic/element_context.rb +9 -12
  28. data/lib/page_magic/element_definition_builder.rb +10 -4
  29. data/lib/page_magic/elements.rb +51 -8
  30. data/lib/page_magic/exceptions.rb +4 -33
  31. data/lib/page_magic/instance_methods.rb +6 -1
  32. data/lib/page_magic/matcher.rb +12 -3
  33. data/lib/page_magic/session.rb +7 -5
  34. data/lib/page_magic/session_methods.rb +2 -0
  35. data/lib/page_magic/utils/string.rb +14 -0
  36. data/lib/page_magic/wait_methods.rb +3 -0
  37. data/lib/page_magic/watcher.rb +2 -0
  38. data/lib/page_magic/watchers.rb +4 -1
  39. data/page_magic.gemspec +19 -13
  40. data/spec/element_spec.rb +2 -0
  41. data/spec/lib/active_support/core_ext/object/to_query_test.rb +78 -0
  42. data/spec/page_magic/class_methods_spec.rb +2 -0
  43. data/spec/page_magic/driver_spec.rb +2 -0
  44. data/spec/page_magic/drivers/poltergeist_spec.rb +2 -0
  45. data/spec/page_magic/drivers/rack_test_spec.rb +2 -0
  46. data/spec/page_magic/drivers/selenium_spec.rb +2 -0
  47. data/spec/page_magic/drivers_spec.rb +2 -0
  48. data/spec/page_magic/element/locators_spec.rb +2 -0
  49. data/spec/page_magic/element/query_builder_spec.rb +110 -0
  50. data/spec/page_magic/element/query_spec.rb +35 -77
  51. data/spec/page_magic/element/selector_spec.rb +14 -7
  52. data/spec/page_magic/element_context_spec.rb +5 -10
  53. data/spec/page_magic/element_definition_builder_spec.rb +3 -1
  54. data/spec/page_magic/elements_spec.rb +19 -5
  55. data/spec/page_magic/instance_methods_spec.rb +2 -0
  56. data/spec/page_magic/matcher_spec.rb +3 -1
  57. data/spec/page_magic/session_methods_spec.rb +2 -0
  58. data/spec/page_magic/session_spec.rb +3 -4
  59. data/spec/page_magic/utils/string_spec.rb +36 -0
  60. data/spec/page_magic/wait_methods_spec.rb +5 -3
  61. data/spec/page_magic/watchers_spec.rb +2 -0
  62. data/spec/page_magic_spec.rb +5 -7
  63. data/spec/spec_helper.rb +3 -0
  64. data/spec/support/shared_contexts.rb +3 -1
  65. data/spec/support/shared_contexts/files_context.rb +2 -0
  66. data/spec/support/shared_contexts/nested_elements_html_context.rb +2 -0
  67. data/spec/support/shared_contexts/rack_application_context.rb +2 -0
  68. data/spec/support/shared_contexts/webapp_fixture_context.rb +3 -1
  69. data/spec/support/shared_examples.rb +2 -0
  70. data/spec/watcher_spec.rb +3 -0
  71. metadata +62 -40
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module PageMagic
2
4
  class Element
3
5
  # class Selector - models the selection criteria understood by Capybara
@@ -10,6 +12,7 @@ module PageMagic
10
12
  def find(name)
11
13
  selector = constants.find { |constant| constant.to_s.casecmp(name.to_s).zero? }
12
14
  raise UnsupportedCriteriaException unless selector
15
+
13
16
  const_get(selector)
14
17
  end
15
18
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module PageMagic
2
4
  class Element
3
5
  # module SelectorMethods - adds method for getting and setting an element selector
@@ -6,6 +8,7 @@ module PageMagic
6
8
  # @param [Hash] selector method for locating the browser element. E.g. text: 'the text'
7
9
  def selector(selector = nil)
8
10
  return @selector unless selector
11
+
9
12
  @selector = selector
10
13
  end
11
14
  end
@@ -1,9 +1,8 @@
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
4
- # Message template for execptions raised as a result of calling method_missing
5
- ELEMENT_NOT_FOUND_MSG = 'Unable to find %s'.freeze
6
-
7
6
  attr_reader :page_element
8
7
 
9
8
  def initialize(page_element)
@@ -24,8 +23,7 @@ module PageMagic
24
23
  prefecteched_element = builder.element
25
24
  return builder.build(prefecteched_element) if prefecteched_element
26
25
 
27
- elements = find(builder)
28
- elements.size == 1 ? elements.first : elements
26
+ find(builder)
29
27
  end
30
28
 
31
29
  def respond_to?(*args)
@@ -35,15 +33,14 @@ module PageMagic
35
33
  private
36
34
 
37
35
  def find(builder)
38
- query_args = builder.build_query
39
- result = page_element.browser_element.all(*query_args)
36
+ query = builder.build_query
37
+ result = query.execute(page_element.browser_element)
40
38
 
41
- if result.empty?
42
- query = Capybara::Query.new(*query_args)
43
- raise ElementMissingException, ELEMENT_NOT_FOUND_MSG % query.description
39
+ if query.multiple_results?
40
+ result.collect { |e| builder.build(e) }
41
+ else
42
+ builder.build(result)
44
43
  end
45
-
46
- result.to_a.collect { |e| builder.build(e) }
47
44
  end
48
45
  end
49
46
  end
@@ -1,8 +1,10 @@
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
6
+ INVALID_SELECTOR_MSG = 'Pass a locator/define one on the class'
7
+ attr_reader :definition_class, :options, :selector, :type, :element, :query_builder
6
8
 
7
9
  def initialize(definition_class:, selector:, type:, options: {}, element: nil)
8
10
  unless element
@@ -13,13 +15,16 @@ module PageMagic
13
15
  @definition_class = definition_class
14
16
  @selector = selector
15
17
  @type = type
16
- @options = options
18
+ @query_builder = Element::QueryBuilder.find(type)
19
+
20
+ @options = { multiple_results: false }.merge(options)
17
21
  @element = element
18
22
  end
19
23
 
20
24
  # @return [Capybara::Query] query to find this element in the browser
21
25
  def build_query
22
- Element::Query.find(type).build(selector, options)
26
+ multiple_results = options.delete(:multiple_results)
27
+ query_builder.build(selector, options, multiple_results: multiple_results)
23
28
  end
24
29
 
25
30
  # Create new instance of the ElementDefinition modeled by this builder
@@ -31,6 +36,7 @@ module PageMagic
31
36
 
32
37
  def ==(other)
33
38
  return false unless other.is_a?(ElementDefinitionBuilder)
39
+
34
40
  this = [options, selector, type, element, definition_class]
35
41
  this == [other.options, other.selector, other.type, other.element, other.definition_class]
36
42
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_support/inflector'
2
4
  require 'page_magic/element_definition_builder'
3
5
  module PageMagic
@@ -12,9 +14,44 @@ module PageMagic
12
14
  end
13
15
  end
14
16
 
15
- INVALID_METHOD_NAME_MSG = 'a method already exists with this method name'.freeze
16
-
17
- TYPES = [:text_field, :button, :link, :checkbox, :select_list, :radios, :textarea].freeze
17
+ INVALID_METHOD_NAME_MSG = 'a method already exists with this method name'
18
+
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
18
55
 
19
56
  class << self
20
57
  def extended(clazz)
@@ -52,8 +89,8 @@ module PageMagic
52
89
  # @param [Hash] selector a key value pair defining the method for locating this element. See above for details
53
90
  def element(*args, &block)
54
91
  block ||= proc {}
55
- options = compute_options(args.dup)
56
- options[:type] = __callee__
92
+ options = compute_options(args.dup, __callee__)
93
+
57
94
  section_class = options.delete(:section_class)
58
95
 
59
96
  add_element_definition(options.delete(:name)) do |parent_element, *e_args|
@@ -61,10 +98,12 @@ module PageMagic
61
98
  parent_element(parent_element)
62
99
  class_exec(*e_args, &block)
63
100
  end
64
- ElementDefinitionBuilder.new(options.merge(definition_class: definition_class))
101
+
102
+ ElementDefinitionBuilder.new(**options.merge(definition_class: definition_class))
65
103
  end
66
104
  end
67
105
 
106
+ alias elements element
68
107
  TYPES.each { |type| alias_method type, :element }
69
108
 
70
109
  # @return [Hash] element definition names mapped to blocks that can be used to create unique instances of
@@ -75,13 +114,17 @@ module PageMagic
75
114
 
76
115
  private
77
116
 
78
- def compute_options(args)
117
+ def compute_options(args, type)
79
118
  section_class = remove_argument(args, Class) || Element
119
+
80
120
  { name: compute_name(args, section_class),
121
+ type: type,
81
122
  selector: compute_selector(args, section_class),
82
123
  options: compute_argument(args, Hash),
83
124
  element: args.delete_at(0),
84
- section_class: section_class }
125
+ section_class: section_class }.tap do |hash|
126
+ hash[:options][:multiple_results] = type.to_s.end_with?('s')
127
+ end
85
128
  end
86
129
 
87
130
  def add_element_definition(name, &block)
@@ -1,39 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module PageMagic
2
4
  class ElementMissingException < RuntimeError
3
5
  end
4
6
 
5
- class InvalidElementNameException < RuntimeError
6
- end
7
-
8
- class InvalidMethodNameException < RuntimeError
9
- end
10
-
11
- class InvalidURLException < RuntimeError
12
- end
13
-
14
- class MatcherInvalidException < RuntimeError
15
- end
16
-
17
- class TimeoutException < RuntimeError
18
- end
19
-
20
- class UnspportedBrowserException < RuntimeError
21
- end
22
-
23
- class UnsupportedCriteriaException < RuntimeError
24
- end
25
-
26
- class UnsupportedSelectorException < RuntimeError
27
- end
28
-
29
- class UndefinedSelectorException < RuntimeError
30
- end
31
-
32
- class NotSupportedException < RuntimeError
33
- end
34
- end
35
- module PageMagic
36
- class ElementMissingException < RuntimeError
7
+ class AmbiguousQueryException < RuntimeError
37
8
  end
38
9
 
39
10
  class InvalidElementNameException < RuntimeError
@@ -51,7 +22,7 @@ module PageMagic
51
22
  class TimeoutException < RuntimeError
52
23
  end
53
24
 
54
- class UnspportedBrowserException < RuntimeError
25
+ class UnsupportedBrowserException < RuntimeError
55
26
  end
56
27
 
57
28
  class UnsupportedCriteriaException < RuntimeError
@@ -1,9 +1,14 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module PageMagic
2
4
  # module InstanceMethods - provides instance level methods for page objects
3
5
  module InstanceMethods
4
6
  attr_reader :browser, :session, :browser_element
5
7
 
6
- include Watchers, SessionMethods, WaitMethods, Element::Locators
8
+ include Element::Locators
9
+ include WaitMethods
10
+ include SessionMethods
11
+ include Watchers
7
12
 
8
13
  # Creates a new instance
9
14
  # @param [Session] session session that provides gateway to the browser throw the users chosen browser
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_support/core_ext/object/to_query'
2
4
  module PageMagic
3
5
  # models mapping used to relate pages to uris
@@ -10,6 +12,7 @@ module PageMagic
10
12
  # @raise [MatcherInvalidException] if at least one component is not specified
11
13
  def initialize(path = nil, parameters: nil, fragment: nil)
12
14
  raise MatcherInvalidException unless path || parameters || fragment
15
+
13
16
  @path = path
14
17
  @parameters = parameters
15
18
  @fragment = fragment
@@ -22,7 +25,7 @@ module PageMagic
22
25
 
23
26
  # @return [String] uri represented by this mapping
24
27
  def compute_uri
25
- path.to_s.tap do |uri|
28
+ path.to_s.dup.tap do |uri|
26
29
  uri << "?#{parameters.to_query}" if parameters
27
30
  uri << "##{fragment}" if fragment
28
31
  end
@@ -44,7 +47,7 @@ module PageMagic
44
47
  # @param [Matcher] other
45
48
  # @return [Fixnum] -1 = smaller, 0 = equal to, 1 = greater than
46
49
  def <=>(other)
47
- results = [:path, :parameters, :fragment].collect do |component|
50
+ results = %i[path parameters fragment].collect do |component|
48
51
  compare(send(component), other.send(component))
49
52
  end
50
53
  results.find { |result| !result.zero? } || 0
@@ -55,6 +58,7 @@ module PageMagic
55
58
  # @return [Boolean]
56
59
  def ==(other)
57
60
  return false unless other.is_a?(Matcher)
61
+
58
62
  path == other.path && parameters == other.parameters && fragment == other.fragment
59
63
  end
60
64
 
@@ -64,11 +68,13 @@ module PageMagic
64
68
 
65
69
  def compare(this, other)
66
70
  return presence_comparison(this, other) unless this && other
71
+
67
72
  fuzzy_comparison(this, other)
68
73
  end
69
74
 
70
75
  def compatible?(string, comparitor)
71
76
  return true if comparitor.nil?
77
+
72
78
  if fuzzy?(comparitor)
73
79
  string =~ comparitor ? true : false
74
80
  else
@@ -82,6 +88,7 @@ module PageMagic
82
88
 
83
89
  def fuzzy?(component)
84
90
  return false unless component
91
+
85
92
  if component.is_a?(Hash)
86
93
  component.values.any? { |o| fuzzy?(o) }
87
94
  else
@@ -108,12 +115,14 @@ module PageMagic
108
115
  def presence_comparison(this, other)
109
116
  return 0 if this.nil? && other.nil?
110
117
  return 1 if this.nil? && other
118
+
111
119
  -1
112
120
  end
113
121
 
114
122
  def query_string_valid?(string)
115
123
  return true unless parameters
116
- !parameters.any? do |key, value|
124
+
125
+ parameters.none? do |key, value|
117
126
  !compatible?(parameters_hash(string)[key.downcase.to_s], value)
118
127
  end
119
128
  end
@@ -1,12 +1,14 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'forwardable'
2
4
  require 'page_magic/matcher'
3
5
  module PageMagic
4
6
  # class Session - coordinates access to the browser though page objects.
5
7
  class Session
6
- URL_MISSING_MSG = 'a path must be mapped or a url supplied'.freeze
7
- REGEXP_MAPPING_MSG = 'URL could not be derived because mapping contains Regexps'.freeze
8
- INVALID_MAPPING_MSG = 'mapping must be a string or regexp'.freeze
9
- UNSUPPORTED_OPERATION_MSG = 'execute_script not supported by driver'.freeze
8
+ URL_MISSING_MSG = 'a path must be mapped or a url supplied'
9
+ REGEXP_MAPPING_MSG = 'URL could not be derived because mapping contains Regexps'
10
+ INVALID_MAPPING_MSG = 'mapping must be a string or regexp'
11
+ UNSUPPORTED_OPERATION_MSG = 'execute_script not supported by driver'
10
12
 
11
13
  extend Forwardable
12
14
 
@@ -18,7 +20,6 @@ module PageMagic
18
20
  def initialize(capybara_session, base_url = nil)
19
21
  @raw_session = capybara_session
20
22
  @base_url = base_url
21
- visit(url: base_url) if base_url
22
23
  @transitions = {}
23
24
  end
24
25
 
@@ -94,6 +95,7 @@ module PageMagic
94
95
  target_url = url || begin
95
96
  if (mapping = transitions.key(page))
96
97
  raise InvalidURLException, REGEXP_MAPPING_MSG unless mapping.can_compute_uri?
98
+
97
99
  url(base_url, mapping.compute_uri)
98
100
  end
99
101
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'forwardable'
2
4
  module PageMagic
3
5
  # module SessionMethods - contains methods for interacting with the {Session}
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PageMagic
4
+ module Utils
5
+ # module String - contains methods for manipulating strings
6
+ module String
7
+ class << self
8
+ def classify(string_or_symbol)
9
+ string_or_symbol.to_s.split('_').collect(&:capitalize).reduce(:+)
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module PageMagic
2
4
  # module WaitMethods - contains methods for waiting
3
5
  module WaitMethods
@@ -10,6 +12,7 @@ module PageMagic
10
12
  start_time = Time.now
11
13
  until Time.now > start_time + timeout_after
12
14
  return true if yield == true
15
+
13
16
  sleep retry_every
14
17
  end
15
18
  raise TimeoutException, 'Action took to long'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module PageMagic
2
4
  # class WatchedElementDefinition - Contains the specification the for checking if an subject has changed
3
5
  class Watcher
@@ -1,9 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'page_magic/watcher'
2
4
 
3
5
  module PageMagic
4
6
  # module Watchers - contains methods for adding watchers and checking them
5
7
  module Watchers
6
- ELEMENT_MISSING_MSG = 'Unable to defined watcher: Element or method with the name %s can not be found'.freeze
8
+ ELEMENT_MISSING_MSG = 'Unable to defined watcher: Element or method with the name %s can not be found'
7
9
 
8
10
  # @param [Symbol] name - the name of the watcher
9
11
  # @return [Boolean] true if a change is detected
@@ -24,6 +26,7 @@ module PageMagic
24
26
  # end
25
27
  def watch(name, method = nil, &block)
26
28
  raise ElementMissingException, (ELEMENT_MISSING_MSG % name) unless block || respond_to?(name)
29
+
27
30
  watched_element = block ? Watcher.new(name, &block) : Watcher.new(name, method)
28
31
  watchers.delete_if { |w| w.name == name }
29
32
  watchers << watched_element.check(self)