page_magic 2.0.0.alpha1 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +5 -2
  3. data/.zsh_config +5 -5
  4. data/Dockerfile +2 -1
  5. data/Gemfile +2 -1
  6. data/Gemfile.lock +9 -5
  7. data/Makefile +7 -3
  8. data/README.md +16 -4
  9. data/VERSION +1 -1
  10. data/lib/active_support/core_ext/object/to_query.rb +6 -6
  11. data/lib/page_magic.rb +15 -16
  12. data/lib/page_magic/class_methods.rb +1 -1
  13. data/lib/page_magic/comparator.rb +37 -0
  14. data/lib/page_magic/comparator/fuzzy.rb +23 -0
  15. data/lib/page_magic/comparator/literal.rb +22 -0
  16. data/lib/page_magic/comparator/null.rb +26 -0
  17. data/lib/page_magic/comparator/parameter_map.rb +52 -0
  18. data/lib/page_magic/drivers.rb +2 -2
  19. data/lib/page_magic/element.rb +19 -8
  20. data/lib/page_magic/element/locators.rb +4 -4
  21. data/lib/page_magic/element/not_found.rb +38 -0
  22. data/lib/page_magic/element/query.rb +19 -27
  23. data/lib/page_magic/element/query/multiple_results.rb +21 -0
  24. data/lib/page_magic/element/query/prefetched_result.rb +26 -0
  25. data/lib/page_magic/element/query/single_result.rb +20 -0
  26. data/lib/page_magic/element/selector.rb +38 -16
  27. data/lib/page_magic/element/selector/methods.rb +18 -0
  28. data/lib/page_magic/element/selector/model.rb +21 -0
  29. data/lib/page_magic/element_context.rb +5 -21
  30. data/lib/page_magic/element_definition_builder.rb +17 -24
  31. data/lib/page_magic/elements.rb +62 -102
  32. data/lib/page_magic/elements/config.rb +103 -0
  33. data/lib/page_magic/elements/inheritance_hooks.rb +15 -0
  34. data/lib/page_magic/elements/types.rb +25 -0
  35. data/lib/page_magic/exceptions.rb +3 -0
  36. data/lib/page_magic/instance_methods.rb +2 -2
  37. data/lib/page_magic/mapping.rb +79 -0
  38. data/lib/page_magic/session.rb +10 -32
  39. data/lib/page_magic/session_methods.rb +1 -1
  40. data/lib/page_magic/transitions.rb +49 -0
  41. data/lib/page_magic/utils/string.rb +4 -0
  42. data/lib/page_magic/utils/url.rb +20 -0
  43. data/lib/page_magic/watcher.rb +10 -17
  44. data/lib/page_magic/watchers.rb +28 -15
  45. data/spec/page_magic/class_methods_spec.rb +64 -37
  46. data/spec/page_magic/comparator/fuzzy_spec.rb +44 -0
  47. data/spec/page_magic/comparator/literal_spec.rb +41 -0
  48. data/spec/page_magic/comparator/null_spec.rb +35 -0
  49. data/spec/page_magic/comparator/parameter_map_spec.rb +75 -0
  50. data/spec/page_magic/driver_spec.rb +25 -29
  51. data/spec/page_magic/drivers/poltergeist_spec.rb +4 -7
  52. data/spec/page_magic/drivers/rack_test_spec.rb +4 -9
  53. data/spec/page_magic/drivers/selenium_spec.rb +9 -12
  54. data/spec/page_magic/drivers_spec.rb +36 -29
  55. data/spec/page_magic/element/locators_spec.rb +26 -25
  56. data/spec/page_magic/element/not_found_spec.rb +24 -0
  57. data/spec/page_magic/element/query/multiple_results_spec.rb +14 -0
  58. data/spec/page_magic/element/query/single_result_spec.rb +21 -0
  59. data/spec/page_magic/element/query_spec.rb +26 -47
  60. data/spec/page_magic/element/selector_spec.rb +118 -110
  61. data/spec/page_magic/element_context_spec.rb +46 -88
  62. data/spec/page_magic/element_definition_builder_spec.rb +12 -71
  63. data/spec/page_magic/element_spec.rb +256 -0
  64. data/spec/page_magic/elements/config_spec.rb +200 -0
  65. data/spec/page_magic/elements_spec.rb +87 -138
  66. data/spec/page_magic/instance_methods_spec.rb +63 -63
  67. data/spec/page_magic/mapping_spec.rb +181 -0
  68. data/spec/page_magic/session_methods_spec.rb +27 -25
  69. data/spec/page_magic/session_spec.rb +109 -198
  70. data/spec/page_magic/transitions_spec.rb +43 -0
  71. data/spec/page_magic/utils/string_spec.rb +20 -27
  72. data/spec/page_magic/utils/url_spec.rb +9 -0
  73. data/spec/page_magic/wait_methods_spec.rb +14 -22
  74. data/spec/page_magic/watcher_spec.rb +22 -0
  75. data/spec/page_magic/watchers_spec.rb +56 -62
  76. data/spec/page_magic_spec.rb +27 -24
  77. data/spec/spec_helper.rb +7 -3
  78. data/spec/support/shared_examples.rb +15 -17
  79. metadata +48 -15
  80. data/lib/page_magic/element/query_builder.rb +0 -61
  81. data/lib/page_magic/element/selector_methods.rb +0 -16
  82. data/lib/page_magic/matcher.rb +0 -130
  83. data/spec/element_spec.rb +0 -251
  84. data/spec/page_magic/element/query_builder_spec.rb +0 -110
  85. data/spec/page_magic/matcher_spec.rb +0 -338
  86. data/spec/support/shared_contexts/files_context.rb +0 -9
  87. data/spec/support/shared_contexts/nested_elements_html_context.rb +0 -18
  88. data/spec/support/shared_contexts/rack_application_context.rb +0 -11
  89. data/spec/support/shared_contexts/webapp_fixture_context.rb +0 -41
  90. data/spec/watcher_spec.rb +0 -64
@@ -1,12 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'forwardable'
4
- require 'page_magic/matcher'
4
+ require_relative 'transitions'
5
+
5
6
  module PageMagic
6
7
  # class Session - coordinates access to the browser though page objects.
7
8
  class Session
8
9
  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
+
10
11
  INVALID_MAPPING_MSG = 'mapping must be a string or regexp'
11
12
  UNSUPPORTED_OPERATION_MSG = 'execute_script not supported by driver'
12
13
 
@@ -20,13 +21,13 @@ module PageMagic
20
21
  def initialize(capybara_session, base_url = nil)
21
22
  @raw_session = capybara_session
22
23
  @base_url = base_url
23
- @transitions = {}
24
+ define_page_mappings({})
24
25
  end
25
26
 
26
27
  # @return [Object] returns page object representing the currently loaded page on the browser. If no mapping
27
28
  # is found then nil returned
28
29
  def current_page
29
- mapping = find_mapped_page(current_url)
30
+ mapping = transitions.mapped_page(current_url)
30
31
  @current_page = initialize_page(mapping) if mapping
31
32
  @current_page
32
33
  end
@@ -50,10 +51,7 @@ module PageMagic
50
51
  # @option transitions [String] path as literal
51
52
  # @option transitions [Regexp] path as a regexp for dynamic matching.
52
53
  def define_page_mappings(transitions)
53
- @transitions = transitions.collect do |key, value|
54
- key = key.is_a?(Matcher) ? key : Matcher.new(key)
55
- [key, value]
56
- end.to_h
54
+ @transitions = Transitions.new(transitions)
57
55
  end
58
56
 
59
57
  # execute javascript on the browser
@@ -74,7 +72,7 @@ module PageMagic
74
72
 
75
73
  # @param args see {::Object#respond_to?}
76
74
  # @return [Boolean] true if self or the current page object responds to the give method name
77
- def respond_to?(*args)
75
+ def respond_to_missing?(*args)
78
76
  super || current_page.respond_to?(*args)
79
77
  end
80
78
 
@@ -92,39 +90,19 @@ module PageMagic
92
90
  # @raise [InvalidURLException] if neither a page or url are supplied
93
91
  # @raise [InvalidURLException] if the mapped path for a page is a Regexp
94
92
  def visit(page = nil, url: nil)
95
- target_url = url || begin
96
- if (mapping = transitions.key(page))
97
- raise InvalidURLException, REGEXP_MAPPING_MSG unless mapping.can_compute_uri?
98
-
99
- url(base_url, mapping.compute_uri)
100
- end
101
- end
93
+ url ||= transitions.url_for(page, base_url: base_url)
102
94
 
103
- raise InvalidURLException, URL_MISSING_MSG unless target_url
95
+ raise InvalidURLException, URL_MISSING_MSG unless url
104
96
 
105
- raw_session.visit(target_url)
97
+ raw_session.visit(url)
106
98
  @current_page = initialize_page(page) if page
107
99
  self
108
100
  end
109
101
 
110
102
  private
111
103
 
112
- def find_mapped_page(url)
113
- matches(url).first
114
- end
115
-
116
- def matches(url)
117
- transitions.keys.find_all { |matcher| matcher.match?(url) }.sort.collect { |match| transitions[match] }
118
- end
119
-
120
104
  def initialize_page(page_class)
121
105
  page_class.new(self).execute_on_load
122
106
  end
123
-
124
- def url(base_url, path)
125
- path = path.sub(%r{^/}, '')
126
- base_url = base_url.sub(%r{/$}, '')
127
- "#{base_url}/#{path}"
128
- end
129
107
  end
130
108
  end
@@ -6,7 +6,7 @@ module PageMagic
6
6
  module SessionMethods
7
7
  extend Forwardable
8
8
 
9
- # @!method execute_script
9
+ # @!method execute_script(script)
10
10
  # execute javascript on the browser
11
11
  # @param [String] script the script to be executed
12
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
@@ -5,6 +5,10 @@ module PageMagic
5
5
  # module String - contains methods for manipulating strings
6
6
  module String
7
7
  class << self
8
+ # convert a snake case `String` or `Symbol`
9
+ # @example
10
+ # classify(:snake_case) # => "SnakeCase"
11
+ # @return [String]
8
12
  def classify(string_or_symbol)
9
13
  string_or_symbol.to_s.split('_').collect(&:capitalize).reduce(:+)
10
14
  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
@@ -3,31 +3,25 @@
3
3
  module PageMagic
4
4
  # class WatchedElementDefinition - Contains the specification the for checking if an subject has changed
5
5
  class Watcher
6
- attr_reader :name, :attribute, :last, :block
6
+ attr_reader :name, :context, :observed_value, :block
7
7
 
8
8
  # @param [Symbol] name the of the subject being checked
9
- # @param [Symbol] method the method that should be called on the subject being checked
10
9
  # @example
11
- # Watcher.new(:text)
12
- # Watcher.new do
10
+ # Watcher.new(:url) do
13
11
  # session.url
14
12
  # end
15
- def initialize(name, method = nil, &block)
13
+ def initialize(name, context:, &block)
16
14
  @name = name
17
- @attribute = method
15
+ @context = context
18
16
  @block = block
19
17
  end
20
18
 
21
- # check current value of watched element. The result of the check is stored against {Watcher#last}
22
- # a block was specified then this will be executed.
23
- # @param [Object] subject - subject to run watcher against
24
- def check(subject = nil)
25
- @last = if block
26
- subject.instance_eval(&block)
27
- else
28
- object = subject.send(name)
29
- attribute ? object.send(attribute) : object
30
- 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)
31
25
  self
32
26
  end
33
27
 
@@ -36,7 +30,6 @@ module PageMagic
36
30
  def ==(other)
37
31
  other.is_a?(Watcher) &&
38
32
  name == other.name &&
39
- attribute == other.attribute &&
40
33
  block == other.block
41
34
  end
42
35
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'page_magic/watcher'
3
+ require_relative 'watcher'
4
4
 
5
5
  module PageMagic
6
6
  # module Watchers - contains methods for adding watchers and checking them
@@ -11,25 +11,27 @@ module PageMagic
11
11
  # @return [Boolean] true if a change is detected
12
12
  def changed?(name)
13
13
  watched_element = watcher(name)
14
- watched_element.last != watched_element.check(self).last
14
+ watched_element.observed_value != watched_element.check.observed_value
15
15
  end
16
16
 
17
17
  # register a new watcher
18
- # @param [Object] name of the watcher/element
19
- # @param [Symbol] method - the method on the watched element to check
20
- # @yieldreturn [Object] the value that should be checked
21
- # @example
22
- # watch(:price, :text)
23
- # @example
24
- # watch(:something) do
18
+ # @overload watch(:price, context: object, method: :text)
19
+ # @param [Symbol] name of the watcher/element
20
+ # @param [Object] context the object that is being watched - defaults to self
21
+ # @param [Symbol] method - the method on the watched element to check
22
+ # @overload watch(:text)
23
+ # @param [Symbol] method - the method on the watched element to check
24
+ # @overload watch(:text, &blk)
25
+ # @param [Symbol] name of the watcher/element
26
+ # @yieldreturn [Object] the value that should be checked
27
+ # @example
28
+ # watch(:something) do
25
29
  # # more complicated code to get value
26
- # end
27
- def watch(name, method = nil, &block)
28
- raise ElementMissingException, (ELEMENT_MISSING_MSG % name) unless block || respond_to?(name)
29
-
30
- watched_element = block ? Watcher.new(name, &block) : Watcher.new(name, method)
30
+ # end
31
+ def watch(name, context: self, method: nil, &blk)
32
+ watcher = blk ? Watcher.new(name, context: context, &blk) : watch_method(name, context: context, method: method)
31
33
  watchers.delete_if { |w| w.name == name }
32
- watchers << watched_element.check(self)
34
+ watchers << watcher.check
33
35
  end
34
36
 
35
37
  # retrieve a watcher given its name
@@ -43,5 +45,16 @@ module PageMagic
43
45
  def watchers
44
46
  @watchers ||= []
45
47
  end
48
+
49
+ private
50
+
51
+ def watch_method(name, context:, method:)
52
+ subject = method || name
53
+ raise ElementMissingException, (ELEMENT_MISSING_MSG % subject) unless context.respond_to?(subject)
54
+
55
+ Watcher.new(name, context: context) do
56
+ public_send(subject)
57
+ end
58
+ end
46
59
  end
47
60
  end
@@ -1,65 +1,92 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module PageMagic
4
- describe ClassMethods do
5
- subject do
3
+ RSpec.describe PageMagic::ClassMethods do
4
+ describe '#load' do
5
+ subject(:page_class) do
6
6
  Class.new.tap do |clazz|
7
7
  clazz.extend(described_class)
8
- clazz.include(InstanceMethods)
8
+ clazz.include(PageMagic::InstanceMethods)
9
9
  end
10
10
  end
11
11
 
12
- describe '#load' do
13
- let(:page_title) { 'page title' }
14
- let(:page_source) do
15
- <<-HTML
12
+ let(:page_title) { 'page title' }
13
+ let(:page_source) do
14
+ <<-HTML
16
15
  <html>
17
16
  <head><title>#{page_title}</title></head>
18
17
  </html>
19
- HTML
20
- end
18
+ HTML
19
+ end
21
20
 
22
- it 'returns an instance using that source' do
23
- expect(subject.load(page_source).title).to eq(page_title)
21
+ it 'returns an instance using that source' do
22
+ expect(page_class.load(page_source).title).to eq(page_title)
23
+ end
24
+ end
25
+
26
+ describe 'on_load' do
27
+ subject(:page_class) do
28
+ Class.new.tap do |clazz|
29
+ clazz.extend(described_class)
24
30
  end
25
31
  end
26
32
 
27
- describe 'on_load' do
28
- context 'block not set' do
29
- it 'returns a default block' do
30
- expect(subject.on_load).to be(described_class::DEFAULT_ON_LOAD)
31
- end
33
+ context 'when a block is not set' do
34
+ it 'returns a default block' do
35
+ expect(page_class.on_load).to be(described_class::DEFAULT_ON_LOAD)
32
36
  end
37
+ end
33
38
 
34
- context 'block set' do
35
- it 'returns that block' do
36
- expected_block = proc {}
37
- subject.on_load(&expected_block)
38
- expect(subject.on_load).to be(expected_block)
39
- end
39
+ context 'when a block is set' do
40
+ it 'returns that block' do
41
+ expected_block = proc {}
42
+ page_class.on_load(&expected_block)
43
+ expect(page_class.on_load).to be(expected_block)
40
44
  end
41
45
  end
46
+ end
42
47
 
43
- describe '#url' do
44
- it 'get/sets a value' do
45
- subject.url(:url)
46
- expect(subject.url).to eq(:url)
48
+ describe '#url' do
49
+ subject(:page_class) do
50
+ Class.new.tap do |clazz|
51
+ clazz.extend(described_class)
47
52
  end
48
53
  end
49
54
 
50
- describe '#visit' do
51
- include_context :webapp_fixture
52
- it 'passes all options to create an active session on the registered url' do
53
- subject.url '/page1'
54
- expect(PageMagic).to receive(:session).with(application: rack_app,
55
- options: {},
56
- browser: :rack_test,
57
- url: subject.url).and_call_original
55
+ it 'get/sets a value' do
56
+ page_class.url(:url)
57
+ expect(page_class.url).to eq(:url)
58
+ end
59
+ end
58
60
 
59
- session = subject.visit(application: rack_app, options: {}, browser: :rack_test)
61
+ describe '#visit' do
62
+ subject(:page_class) do
63
+ Class.new.tap do |clazz|
64
+ clazz.extend(described_class)
65
+ clazz.include(PageMagic::InstanceMethods)
66
+ clazz.url ''
67
+ end
68
+ end
60
69
 
61
- expect(session.title).to eq('page1')
70
+ let(:rack_app) do
71
+ Class.new do
72
+ def self.call(_env)
73
+ [200, {}, ['<html><head><title>page1</title></head></html>']]
74
+ end
62
75
  end
63
76
  end
77
+
78
+ it 'passes all options to create an active session on the registered url' do
79
+ allow(PageMagic).to receive(:session).and_call_original
80
+
81
+ page_class.visit(application: rack_app, options: {}, browser: :rack_test)
82
+
83
+ expected_option = { application: rack_app, options: {}, browser: :rack_test, url: page_class.url }
84
+ expect(PageMagic).to have_received(:session).with(expected_option)
85
+ end
86
+
87
+ it 'returns a session' do
88
+ session = page_class.visit(application: rack_app, options: {}, browser: :rack_test)
89
+ expect(session).to be_kind_of(PageMagic::Session)
90
+ end
64
91
  end
65
92
  end
@@ -0,0 +1,44 @@
1
+ RSpec.describe PageMagic::Comparator::Fuzzy do
2
+ describe '#fuzzy?' do
3
+ context 'when one value is fuzzy' do
4
+ it 'returns true' do
5
+ map = described_class.new(//)
6
+ expect(map).to be_fuzzy
7
+ end
8
+ end
9
+ end
10
+
11
+ describe 'match?' do
12
+ context 'when comparator contains the parameter' do
13
+ it 'returns true' do
14
+ expect(described_class.new(/f*o/)).to be_match('foo')
15
+ end
16
+ end
17
+
18
+ context 'when comparator does not contains the parameter' do
19
+ it 'returns false' do
20
+ expect(described_class.new(/f*o/)).not_to be_match('bar')
21
+ end
22
+ end
23
+ end
24
+
25
+ describe '#<=>' do
26
+ context 'when other is `Null`' do
27
+ it 'is lesser' do
28
+ expect(described_class.new(//) <=> PageMagic::Comparator::Null.new).to be(-1)
29
+ end
30
+ end
31
+
32
+ context 'when other is `Fuzzy`' do
33
+ it 'is equal' do
34
+ expect(described_class.new(//) <=> described_class.new(//)).to be 0
35
+ end
36
+ end
37
+
38
+ context 'when other is `Literal`' do
39
+ it 'is greater' do
40
+ expect(described_class.new(//) <=> PageMagic::Comparator::Literal.new('/')).to be 1
41
+ end
42
+ end
43
+ end
44
+ end