page_magic 2.0.0.alpha1 → 2.0.0

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