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
data/Rakefile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  Bundler.require :test, :development
2
4
 
3
5
  RuboCop::RakeTask.new
@@ -12,11 +14,19 @@ Jeweler::Tasks.new do |gem|
12
14
  gem.license = 'ruby'
13
15
  gem.summary = 'Framework for modeling and interacting with webpages'
14
16
  gem.description = 'Framework for modeling and interacting with webpages which wraps capybara'
15
- gem.email = 'info@lad-tech.com'
17
+ gem.email = 'info@lvl-up.uk'
16
18
  gem.authors = ['Leon Davis']
17
19
  gem.required_ruby_version = '>= 2.1'
18
20
  end
19
21
 
20
22
  Jeweler::RubygemsDotOrgTasks.new
21
23
 
22
- task default: [:spec, 'rubocop:auto_correct']
24
+ require 'rake/testtask'
25
+ Rake::TestTask.new do |t|
26
+ t.libs << 'spec'
27
+ t.pattern = 'spec/**/*_test.rb'
28
+ t.warning = true
29
+ t.verbose = true
30
+ end
31
+
32
+ task default: [:spec, :test, 'rubocop:auto_correct']
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.2.6
1
+ 2.0.0.alpha1
data/circle.yml CHANGED
@@ -1,6 +1,8 @@
1
1
  machine:
2
2
  ruby:
3
- version: '2.1'
3
+ version: '2.2'
4
4
  test:
5
5
  override:
6
- - bundle exec rake
6
+ - bundle exec rake
7
+ post:
8
+ - CODECLIMATE_REPO_TOKEN=$CODECLIMATE_REPO_TOKEN bundle exec codeclimate-test-reporter
@@ -0,0 +1,84 @@
1
+ require 'cgi'
2
+
3
+ class Object
4
+ # Alias of <tt>to_s</tt>.
5
+ def to_param
6
+ to_s
7
+ end
8
+
9
+ # Converts an object into a string suitable for use as a URL query string,
10
+ # using the given <tt>key</tt> as the param name.
11
+ def to_query(key)
12
+ "#{CGI.escape(key.to_param)}=#{CGI.escape(to_param.to_s)}"
13
+ end
14
+ end
15
+
16
+ class NilClass
17
+ # Returns +self+.
18
+ def to_param
19
+ self
20
+ end
21
+ end
22
+
23
+ class TrueClass
24
+ # Returns +self+.
25
+ def to_param
26
+ self
27
+ end
28
+ end
29
+
30
+ class FalseClass
31
+ # Returns +self+.
32
+ def to_param
33
+ self
34
+ end
35
+ end
36
+
37
+ class Array
38
+ # Calls <tt>to_param</tt> on all its elements and joins the result with
39
+ # slashes. This is used by <tt>url_for</tt> in Action Pack.
40
+ def to_param
41
+ collect(&:to_param).join '/'
42
+ end
43
+
44
+ # Converts an array into a string suitable for use as a URL query string,
45
+ # using the given +key+ as the param name.
46
+ #
47
+ # ['Rails', 'coding'].to_query('hobbies') # => "hobbies%5B%5D=Rails&hobbies%5B%5D=coding"
48
+ def to_query(key)
49
+ prefix = "#{key}[]"
50
+
51
+ if empty?
52
+ nil.to_query(prefix)
53
+ else
54
+ collect { |value| value.to_query(prefix) }.join '&'
55
+ end
56
+ end
57
+ end
58
+
59
+ class Hash
60
+ # Returns a string representation of the receiver suitable for use as a URL
61
+ # query string:
62
+ #
63
+ # {name: 'David', nationality: 'Danish'}.to_query
64
+ # # => "name=David&nationality=Danish"
65
+ #
66
+ # An optional namespace can be passed to enclose key names:
67
+ #
68
+ # {name: 'David', nationality: 'Danish'}.to_query('user')
69
+ # # => "user%5Bname%5D=David&user%5Bnationality%5D=Danish"
70
+ #
71
+ # The string pairs "key=value" that conform the query string
72
+ # are sorted lexicographically in ascending order.
73
+ #
74
+ # This method is also aliased as +to_param+.
75
+ def to_query(namespace = nil)
76
+ collect do |key, value|
77
+ unless (value.is_a?(Hash) || value.is_a?(Array)) && value.empty?
78
+ value.to_query(namespace ? "#{namespace}[#{key}]" : key)
79
+ end
80
+ end.compact.sort! * '&'
81
+ end
82
+
83
+ alias to_param to_query
84
+ end
data/lib/page_magic.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  $LOAD_PATH.unshift(File.dirname(__FILE__).to_s)
2
4
  require 'capybara'
3
5
  require 'page_magic/exceptions'
@@ -31,7 +33,8 @@ module PageMagic
31
33
  def included(clazz)
32
34
  clazz.class_eval do
33
35
  include(InstanceMethods)
34
- extend(Elements, ClassMethods)
36
+ extend ClassMethods
37
+ extend Elements
35
38
  end
36
39
  end
37
40
 
@@ -48,10 +51,10 @@ module PageMagic
48
51
  # @param [Symbol] browser name of browser
49
52
  # @param [String] url url to start the session on
50
53
  # @param [Hash] options browser driver specific options
51
- # @return [Session] configured sessoin
52
- def session(application: nil, browser: :rack_test, url:, options: {})
54
+ # @return [Session] configured session
55
+ def session(url: nil, application: nil, browser: :rack_test, options: {})
53
56
  driver = drivers.find(browser)
54
- raise UnspportedBrowserException unless driver
57
+ raise UnsupportedBrowserException unless driver
55
58
 
56
59
  Capybara.register_driver browser do |app|
57
60
  driver.build(app, browser: browser, options: options)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module PageMagic
2
4
  # module ClassMethods - contains class level methods for PageObjects
3
5
  module ClassMethods
@@ -15,6 +17,7 @@ module PageMagic
15
17
  # if one has not been set on the page object class it will return a default block that does nothing
16
18
  def on_load(&block)
17
19
  return @on_load || DEFAULT_ON_LOAD unless block
20
+
18
21
  @on_load = block
19
22
  end
20
23
 
@@ -34,7 +37,7 @@ module PageMagic
34
37
  session_options = { browser: browser, options: options, url: url }
35
38
  session_options[:application] = application if application
36
39
 
37
- PageMagic.session(session_options).tap do |session|
40
+ PageMagic.session(**session_options).tap do |session|
38
41
  session.visit(self, url: url)
39
42
  end
40
43
  end
@@ -1,7 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module PageMagic
2
4
  # class Driver - instances are factories for drivers used by PageMagic
3
5
  class Driver
4
6
  attr_reader :supported_browsers, :handler
7
+
5
8
  # Creates a driver definition
6
9
  # @example
7
10
  # Driver.new do |rack_application, options|
@@ -1,4 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'page_magic/driver'
4
+ require 'page_magic/utils/string'
2
5
  module PageMagic
3
6
  # class Drivers - creates an object that can be used to hold driver definitions
4
7
  # These PageMagic gets the user's chosen driver from this object.
@@ -16,12 +19,10 @@ module PageMagic
16
19
  # Loads drivers defined in files at the given path
17
20
  # @param [String] path where the drivers are located
18
21
  def load(path = "#{__dir__}/drivers")
19
- require 'active_support/inflector'
20
-
21
- Dir["#{path}/*.rb"].each do |driver_file|
22
+ Dir["#{path}/*.rb"].sort.each do |driver_file|
22
23
  require driver_file
23
24
  driver_name = File.basename(driver_file)[/(.*)\.rb$/, 1]
24
- register self.class.const_get(driver_name.classify)
25
+ register self.class.const_get(Utils::String.classify(driver_name))
25
26
  end
26
27
  end
27
28
 
@@ -35,7 +36,7 @@ module PageMagic
35
36
  # @param [Object] other subject of equality check
36
37
  # @return [Boolean] true if the object is a match
37
38
  def ==(other)
38
- other.is_a?(Drivers) && all == other.all
39
+ other.respond_to?(:all) && all == other.all
39
40
  end
40
41
  end
41
42
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  PageMagic::Drivers::Poltergeist = PageMagic::Driver.new(:poltergeist) do |app, options|
2
4
  require 'capybara/poltergeist'
3
5
  Capybara::Poltergeist::Driver.new(app, options)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  PageMagic::Drivers::RackTest = PageMagic::Driver.new(:rack_test) do |app, options|
2
- Capybara::RackTest::Driver.new(app, options)
4
+ Capybara::RackTest::Driver.new(app, **options)
3
5
  end
@@ -1,4 +1,6 @@
1
+ # frozen_string_literal: true
2
+
1
3
  PageMagic::Drivers::Selenium = PageMagic::Driver.new(:chrome, :firefox) do |app, options, browser|
2
- require 'watir-webdriver'
3
- Capybara::Selenium::Driver.new(app, options.dup.merge(browser: browser))
4
+ require 'watir'
5
+ Capybara::Selenium::Driver.new(app, **options.dup.merge(browser: browser))
4
6
  end
@@ -1,17 +1,25 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'forwardable'
2
4
  require 'page_magic/element/selector_methods'
3
5
  require 'page_magic/element/locators'
4
6
  require 'page_magic/element/selector'
5
- require 'page_magic/element/query'
7
+ require 'page_magic/element/query_builder'
6
8
  module PageMagic
7
9
  # class Element - represents an element in a html page.
8
10
  class Element
9
- EVENT_TYPES = [:set, :select, :select_option, :unselect_option, :click].freeze
11
+ EVENT_TYPES = %i[set select select_option unselect_option click].freeze
10
12
  DEFAULT_HOOK = proc {}.freeze
11
- EVENT_NOT_SUPPORTED_MSG = '%s event not supported'.freeze
13
+ EVENT_NOT_SUPPORTED_MSG = '%s event not supported'
12
14
 
13
- include SelectorMethods, Watchers, SessionMethods, WaitMethods, Locators
14
- extend Elements, SelectorMethods, Forwardable
15
+ include Locators
16
+ include WaitMethods
17
+ include SessionMethods
18
+ include Watchers
19
+ include SelectorMethods
20
+ extend Forwardable
21
+ extend SelectorMethods
22
+ extend Elements
15
23
 
16
24
  attr_reader :type, :name, :parent_element, :browser_element, :before_events, :after_events
17
25
 
@@ -24,7 +32,7 @@ module PageMagic
24
32
  # @!method before_events
25
33
  # If a block is passed in, it adds it to be run before an event is triggered on an element.
26
34
  # @see .after_events
27
- %i(after_events before_events).each do |method|
35
+ %i[after_events before_events].each do |method|
28
36
  define_method method do |&block|
29
37
  instance_variable_name = "@#{method}".to_sym
30
38
  instance_variable_value = instance_variable_get(instance_variable_name) || [DEFAULT_HOOK]
@@ -38,6 +46,7 @@ module PageMagic
38
46
  # @return [Element]
39
47
  def parent_element(page_element = nil)
40
48
  return @parent_page_element unless page_element
49
+
41
50
  @parent_page_element = page_element
42
51
  end
43
52
 
@@ -78,9 +87,7 @@ module PageMagic
78
87
  # @raise [NotSupportedException] if the wrapped Capybara element does not support the method
79
88
  EVENT_TYPES.each do |method|
80
89
  define_method method do |*args|
81
- unless browser_element.respond_to?(method)
82
- raise NotSupportedException, EVENT_NOT_SUPPORTED_MSG % method
83
- end
90
+ raise NotSupportedException, EVENT_NOT_SUPPORTED_MSG % method unless browser_element.respond_to?(method)
84
91
 
85
92
  browser_element.send(method, *args)
86
93
  end
@@ -91,7 +98,8 @@ module PageMagic
91
98
  rescue ElementMissingException
92
99
  begin
93
100
  return browser_element.send(method, *args, &block) if browser_element.respond_to?(method)
94
- return parent_element.send(method, *args, &block)
101
+
102
+ parent_element.send(method, *args, &block)
95
103
  rescue NoMethodError
96
104
  super
97
105
  end
@@ -131,6 +139,7 @@ module PageMagic
131
139
  def wrap_events(raw_element)
132
140
  EVENT_TYPES.each do |action_method|
133
141
  next unless raw_element.respond_to?(action_method)
142
+
134
143
  apply_hooks(raw_element: raw_element,
135
144
  capybara_method: action_method,
136
145
  before_events: before_events,
@@ -1,10 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module PageMagic
2
4
  # for the benefit of pull review :@
3
5
  class Element
4
6
  # contains method for finding element definitions
5
7
  module Locators
6
8
  # message used when raising {ElementMissingException} from methods within this module
7
- ELEMENT_NOT_DEFINED_MSG = 'Element not defined: %s'.freeze
9
+ ELEMENT_NOT_DEFINED_MSG = 'Element not defined: %s'
8
10
 
9
11
  # find an element definition based on its name
10
12
  # @param [Symbol] name name of the element
@@ -13,6 +15,7 @@ module PageMagic
13
15
  def element_by_name(name, *args)
14
16
  defintion = element_definitions[name]
15
17
  raise ElementMissingException, (ELEMENT_NOT_DEFINED_MSG % name) unless defintion
18
+
16
19
  defintion.call(self, *args)
17
20
  end
18
21
 
@@ -1,45 +1,45 @@
1
- require 'capybara/query'
1
+ # frozen_string_literal: true
2
+
2
3
  module PageMagic
3
4
  class Element
4
- # class Query - models overall queries for Capybara, queries can include:
5
- # - requirements on element type
6
- # - selection criteria, modeled through the Selector class
7
- # - options
5
+ # class Query - executes query on capybara driver
8
6
  class Query
9
- class << self
10
- # Find a query using it's name
11
- # @param [Symbol] type the name of the required query in snakecase format
12
- # @return [Query] returns the predefined query with the given name
13
- def find(type)
14
- query = constants.find { |constant| constant.to_s.casecmp(type.to_s).zero? }
15
- return ELEMENT unless query
16
- const_get(query)
17
- end
18
- end
7
+ # Message template for execptions raised as a result of calling method_missing
8
+ ELEMENT_NOT_FOUND_MSG = 'Unable to find %s'
9
+
10
+ attr_reader :args, :multiple_results
19
11
 
20
- attr_reader :type
12
+ alias multiple_results? multiple_results
21
13
 
22
- # @param type -
23
- def initialize(type = nil)
24
- @type = type
14
+ def initialize(args, multiple_results: false)
15
+ @args = args
16
+ @multiple_results = multiple_results
25
17
  end
26
18
 
27
- # Build query parameters for Capybara's find method
28
- # @param [Hash] locator the location method e.g. text: 'button text'
29
- # @param [Hash] options additional options to be provided to Capybara. e.g. count: 3
30
- # @return [Array] list of compatible capybara query parameters.
31
- def build(locator, options = {})
32
- [].tap do |array|
33
- selector = Selector.find(locator.keys.first)
34
- array << selector.build(type, locator.values.first)
35
- array << options unless options.empty?
36
- end.flatten
19
+ def execute(capybara_element)
20
+ if multiple_results
21
+ capybara_element.all(*args).to_a.tap do |result|
22
+ raise Capybara::ElementNotFound if result.empty?
23
+ end
24
+ else
25
+ # TODO - make sure there is a test around this.
26
+ if args.last.is_a?(Hash)
27
+ capybara_element.find(*args[0...-1], **args.last)
28
+ else
29
+ capybara_element.find(*args)
30
+ end
31
+
32
+
33
+ end
34
+ rescue Capybara::Ambiguous => e
35
+ raise AmbiguousQueryException, e.message
36
+ rescue Capybara::ElementNotFound => e
37
+ raise ElementMissingException, e.message
37
38
  end
38
39
 
39
- ELEMENT = Query.new
40
- TEXT_FIELD = CHECKBOX = SELECT_LIST = RADIOS = TEXTAREA = Query.new(:field)
41
- LINK = Query.new(:link)
42
- BUTTON = Query.new(:button)
40
+ def ==(other)
41
+ other.respond_to?(:args) && args == other.args
42
+ end
43
43
  end
44
44
  end
45
45
  end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'page_magic/element/query'
4
+ module PageMagic
5
+ class Element
6
+ # class QueryBuilder - builds query to be executed on capybara driver, queries can include:
7
+ # - requirements on element type
8
+ # - selection criteria, modeled through the Selector class
9
+ # - options
10
+ class QueryBuilder
11
+ class << self
12
+ # Find a query using it's name
13
+ # @param [Symbol] type the name of the required query in snakecase format
14
+ # @return [QueryBuilder] returns the predefined query with the given name
15
+ def find(type)
16
+ query = constants.find { |constant| constant.to_s.casecmp(type.to_s).zero? }
17
+ return ELEMENT unless query
18
+
19
+ const_get(query)
20
+ end
21
+ end
22
+
23
+ attr_reader :type
24
+
25
+ # @param type -
26
+ def initialize(type = nil)
27
+ @type = type
28
+ end
29
+
30
+ # Build query parameters for Capybara's find method
31
+ # @param [Hash] locator the location method e.g. text: 'button text'
32
+ # @param [Hash] capybara_options additional options to be provided to Capybara. e.g. count: 3
33
+ # @return [Array] list of compatible capybara query parameters.
34
+ def build(locator, capybara_options = {}, multiple_results: false)
35
+ args = [].tap do |array|
36
+ selector = Selector.find(locator.keys.first)
37
+ array << selector.build(type, locator.values.first)
38
+ array << capybara_options unless capybara_options.empty?
39
+ end.flatten
40
+
41
+ Query.new(args, multiple_results: multiple_results)
42
+ end
43
+
44
+ ELEMENT = QueryBuilder.new
45
+ TEXT_FIELD =
46
+ CHECKBOX =
47
+ SELECT_LIST =
48
+ RADIO =
49
+ TEXTAREA =
50
+ FIELD =
51
+ FILE_FIELD =
52
+ FILLABLE_FIELD =
53
+ RADIO_BUTTON =
54
+ SELECT = QueryBuilder.new(:field)
55
+
56
+ LINK = QueryBuilder.new(:link)
57
+ LABEL = QueryBuilder.new(:label)
58
+ BUTTON = QueryBuilder.new(:button)
59
+ end
60
+ end
61
+ end