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,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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module PageMagic
2
4
  class ElementMissingException < RuntimeError
3
5
  end
@@ -20,7 +22,7 @@ module PageMagic
20
22
  class TimeoutException < RuntimeError
21
23
  end
22
24
 
23
- class UnspportedBrowserException < RuntimeError
25
+ class UnsupportedBrowserException < RuntimeError
24
26
  end
25
27
 
26
28
  class UnsupportedCriteriaException < RuntimeError
@@ -34,4 +36,7 @@ module PageMagic
34
36
 
35
37
  class NotSupportedException < RuntimeError
36
38
  end
39
+
40
+ class InvalidConfigurationException < StandardError
41
+ end
37
42
  end
@@ -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
@@ -14,7 +19,7 @@ module PageMagic
14
19
  @browser_element = browser
15
20
  end
16
21
 
17
- # @return [Array] class level defined element definitions
22
+ # @return [Array<ElementDefinitionBuilder>] class level defined element definitions
18
23
  def element_definitions
19
24
  self.class.element_definitions
20
25
  end
@@ -32,7 +37,7 @@ module PageMagic
32
37
  element_context.send(method, *args)
33
38
  end
34
39
 
35
- def respond_to?(*args)
40
+ def respond_to_missing?(*args)
36
41
  contains_element?(args.first) || super
37
42
  end
38
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
@@ -1,12 +1,15 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'forwardable'
2
- require 'page_magic/matcher'
4
+ require_relative 'transitions'
5
+
3
6
  module PageMagic
4
7
  # class Session - coordinates access to the browser though page objects.
5
8
  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
9
+ URL_MISSING_MSG = 'a path must be mapped or a url supplied'
10
+
11
+ INVALID_MAPPING_MSG = 'mapping must be a string or regexp'
12
+ UNSUPPORTED_OPERATION_MSG = 'execute_script not supported by driver'
10
13
 
11
14
  extend Forwardable
12
15
 
@@ -18,14 +21,13 @@ module PageMagic
18
21
  def initialize(capybara_session, base_url = nil)
19
22
  @raw_session = capybara_session
20
23
  @base_url = base_url
21
- visit(url: base_url) if base_url
22
- @transitions = {}
24
+ define_page_mappings({})
23
25
  end
24
26
 
25
27
  # @return [Object] returns page object representing the currently loaded page on the browser. If no mapping
26
28
  # is found then nil returned
27
29
  def current_page
28
- mapping = find_mapped_page(current_url)
30
+ mapping = transitions.mapped_page(current_url)
29
31
  @current_page = initialize_page(mapping) if mapping
30
32
  @current_page
31
33
  end
@@ -49,10 +51,7 @@ module PageMagic
49
51
  # @option transitions [String] path as literal
50
52
  # @option transitions [Regexp] path as a regexp for dynamic matching.
51
53
  def define_page_mappings(transitions)
52
- @transitions = transitions.collect do |key, value|
53
- key = key.is_a?(Matcher) ? key : Matcher.new(key)
54
- [key, value]
55
- end.to_h
54
+ @transitions = Transitions.new(transitions)
56
55
  end
57
56
 
58
57
  # execute javascript on the browser
@@ -73,7 +72,7 @@ module PageMagic
73
72
 
74
73
  # @param args see {::Object#respond_to?}
75
74
  # @return [Boolean] true if self or the current page object responds to the give method name
76
- def respond_to?(*args)
75
+ def respond_to_missing?(*args)
77
76
  super || current_page.respond_to?(*args)
78
77
  end
79
78
 
@@ -91,38 +90,19 @@ module PageMagic
91
90
  # @raise [InvalidURLException] if neither a page or url are supplied
92
91
  # @raise [InvalidURLException] if the mapped path for a page is a Regexp
93
92
  def visit(page = nil, url: nil)
94
- target_url = url || begin
95
- if (mapping = transitions.key(page))
96
- raise InvalidURLException, REGEXP_MAPPING_MSG unless mapping.can_compute_uri?
97
- url(base_url, mapping.compute_uri)
98
- end
99
- end
93
+ url ||= transitions.url_for(page, base_url: base_url)
100
94
 
101
- raise InvalidURLException, URL_MISSING_MSG unless target_url
95
+ raise InvalidURLException, URL_MISSING_MSG unless url
102
96
 
103
- raw_session.visit(target_url)
97
+ raw_session.visit(url)
104
98
  @current_page = initialize_page(page) if page
105
99
  self
106
100
  end
107
101
 
108
102
  private
109
103
 
110
- def find_mapped_page(url)
111
- matches(url).first
112
- end
113
-
114
- def matches(url)
115
- transitions.keys.find_all { |matcher| matcher.match?(url) }.sort.collect { |match| transitions[match] }
116
- end
117
-
118
104
  def initialize_page(page_class)
119
105
  page_class.new(self).execute_on_load
120
106
  end
121
-
122
- def url(base_url, path)
123
- path = path.sub(%r{^/}, '')
124
- base_url = base_url.sub(%r{/$}, '')
125
- "#{base_url}/#{path}"
126
- end
127
107
  end
128
108
  end
@@ -1,10 +1,12 @@
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}
4
6
  module SessionMethods
5
7
  extend Forwardable
6
8
 
7
- # @!method execute_script
9
+ # @!method execute_script(script)
8
10
  # execute javascript on the browser
9
11
  # @param [String] script the script to be executed
10
12
  # @return [Object] object returned by the {Session#execute_script}
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'utils/url'
4
+ require_relative 'mapping'
5
+
6
+ module PageMagic
7
+ # class Transitions - used for registering the page classes that should be used against particular paths
8
+ class Transitions < Hash
9
+ REGEXP_MAPPING_MSG = 'URL could not be derived because mapping contains Regexps'
10
+
11
+ # Create a new transitions object.
12
+ # @param [Hash{String,PageMagic::Mapping => PageMagic}] transitions a map of paths to pages
13
+ # @example
14
+ # Transitions.new('/path1' => Page1, Matcher.new('/another_*') => AnotherPageClass )
15
+ def initialize(transitions)
16
+ super
17
+ transitions.each do |key, value|
18
+ key = key.is_a?(Mapping) ? key : Mapping.new(key)
19
+ self[key] = value
20
+ end
21
+ end
22
+
23
+ # get the url to be used when visiting the path mapped against the given page
24
+ # @param [PageMagic] page - the page class to get the mapped path from
25
+ # @param [String] base_url - the base url of the site to be joined to the mapped path
26
+ # @return String
27
+ # @raise InvalidURLException - Raised if it is not possible to generate the url for the mapped page
28
+ # i.e. if the mapping is a regular expression.
29
+ def url_for(page, base_url:)
30
+ return unless (mapping = key(page))
31
+ raise InvalidURLException, REGEXP_MAPPING_MSG unless mapping.can_compute_uri?
32
+
33
+ PageMagic::Utils::URL.concat(base_url, mapping.compute_uri)
34
+ end
35
+
36
+ # get the page class mapped to the given url
37
+ # @param [String] url - the url to search against
38
+ # @return [PageMagic]
39
+ def mapped_page(url)
40
+ matches(url).first
41
+ end
42
+
43
+ private
44
+
45
+ def matches(url)
46
+ keys.find_all { |matcher| matcher.match?(url) }.sort.collect { |match| self[match] }
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,18 @@
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
+ # convert a snake case `String` or `Symbol`
9
+ # @example
10
+ # classify(:snake_case) # => "SnakeCase"
11
+ # @return [String]
12
+ def classify(string_or_symbol)
13
+ string_or_symbol.to_s.split('_').collect(&:capitalize).reduce(:+)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PageMagic
4
+ module Utils
5
+ # module String - contains methods for manipulating strings
6
+ module URL
7
+ class << self
8
+ # build a url from a base and path.
9
+ # @example
10
+ # concat('http://base.url/', '/path') # => "http://base.url/path"
11
+ # @return [String]
12
+ def concat(base_url, path)
13
+ path = path.sub(%r{^/}, '')
14
+ base_url = base_url.sub(%r{/$}, '')
15
+ "#{base_url}/#{path}"
16
+ end
17
+ end
18
+ end
19
+ end
20
+ 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,31 +1,27 @@
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
4
- attr_reader :name, :attribute, :last, :block
6
+ attr_reader :name, :context, :observed_value, :block
5
7
 
6
8
  # @param [Symbol] name the of the subject being checked
7
- # @param [Symbol] method the method that should be called on the subject being checked
8
9
  # @example
9
- # Watcher.new(:text)
10
- # Watcher.new do
10
+ # Watcher.new(:url) do
11
11
  # session.url
12
12
  # end
13
- def initialize(name, method = nil, &block)
13
+ def initialize(name, context:, &block)
14
14
  @name = name
15
- @attribute = method
15
+ @context = context
16
16
  @block = block
17
17
  end
18
18
 
19
- # check current value of watched element. The result of the check is stored against {Watcher#last}
20
- # a block was specified then this will be executed.
21
- # @param [Object] subject - subject to run watcher against
22
- def check(subject = nil)
23
- @last = if block
24
- subject.instance_eval(&block)
25
- else
26
- object = subject.send(name)
27
- attribute ? object.send(attribute) : object
28
- end
19
+ # check current value of watched element. The result of the check can be accessed
20
+ # by calling {PageMagic::Watcher#last}
21
+ # if a block was specified to the constructor then this will be executed.
22
+ # @return [PageMagic::Watcher]
23
+ def check
24
+ @observed_value = context.instance_eval(&block)
29
25
  self
30
26
  end
31
27
 
@@ -34,7 +30,6 @@ module PageMagic
34
30
  def ==(other)
35
31
  other.is_a?(Watcher) &&
36
32
  name == other.name &&
37
- attribute == other.attribute &&
38
33
  block == other.block
39
34
  end
40
35
  end