page_magic 1.2.8 → 2.0.2

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